CVE-2021-3492
라온화이트햇 핵심연구팀 조진호
CVE-2021-3492
Summary
Pwn2Own Vancouver 2021에서 Ubuntu Shiftfs driver발생한 double free 취약점. KASLR
, SMAP
를 우회하여 LPE에 성공했다. Groovy(20.10)와 Focal(20.04) 데스크탑, 서버 모두 해당한다.
- Groovy version is corrected since kernel version : 5.8.0-50.56
- Focal version is corrected since kernel version : 5.4.0-72.80
shiftfs Driver
취약점은 파일시스템 드라이버인 shiftfs
에서 터진다. 이 드라이버의 기능은 파일시스템을 마운트하고 디렉토리에 UID, GID를 마운트 한 소유자의 것으로 바꿔주는 기능을 한다.
static struct file_system_type shiftfs_type = {
.owner = THIS_MODULE,
.name = "shiftfs",
.mount = shiftfs_mount,
.kill_sb = kill_anon_super,
.fs_flags = FS_USERNS_MOUNT,
};
간략하게 기능을 보자면 fs/shiftfs.c
파일에 모든 코드들이 있어 보기 편하다. 먼저 마운트 하게 되면 shiftfs_mount
함수가 실행된다.
static struct dentry *shiftfs_mount(struct file_system_type *fs_type,
int flags, const char *dev_name, void *data)
{
struct shiftfs_data d = { data, dev_name };
return mount_nodev(fs_type, flags, &d, shiftfs_fill_super);
}
그리고 shared memory에 매핑하는 커널 함수인 mount_nodev
를 이용해 마운트 한다. shiftfs_fill_super
는 리눅스 파일시스템의 루트와 같은 superblock
을 파라미터로 가져와 새 가상 파일 시스템을 구현하는 함수로 드라이버에서 구현해야하는 함수이다.
static int shiftfs_fill_super(struct super_block *sb, void *raw_data,
int silent)
{
...
inode = new_inode(sb);
if (!inode) {
err = -ENOMEM;
goto out_put_path;
}
shiftfs_fill_inode(inode, dentry->d_inode->i_ino, S_IFDIR, 0, dentry);
그리고 superblock
뒤의 데이터를 읽어서 리눅스에서 사용할 수 있도록 inode로 만든다.
static void shiftfs_fill_inode(struct inode *inode, unsigned long ino,
umode_t mode, dev_t dev, struct dentry *dentry)
{
struct inode *loweri;
inode->i_ino = ino;
inode->i_flags |= S_NOCMTIME;
mode &= S_IFMT;
inode->i_mode = mode;
switch (mode & S_IFMT) {
case S_IFDIR:
inode->i_op = &shiftfs_dir_inode_operations;
inode->i_fop = &shiftfs_dir_operations;
break;
case S_IFLNK:
inode->i_op = &shiftfs_symlink_inode_operations;
break;
case S_IFREG:
inode->i_op = &shiftfs_file_inode_operations;
inode->i_fop = &shiftfs_file_operations;
inode->i_mapping->a_ops = &shiftfs_aops;
break;
default:
inode->i_op = &shiftfs_special_inode_operations;
init_special_inode(inode, mode, dev);
break;
}
if (!dentry)
return;
loweri = dentry->d_inode;
if (!loweri->i_op->get_link)
inode->i_opflags |= IOP_NOFOLLOW;
shiftfs_copyattr(loweri, inode);
shiftfs_copyflags(loweri, inode);
set_nlink(inode, loweri->i_nlink);
}
디렉토리, 심볼릭 링크, 파일 등 타입별로 inode
를 만든다.
const struct file_operations shiftfs_file_operations = {
.open = shiftfs_open,
.release = shiftfs_release,
.llseek = shiftfs_file_llseek,
.read_iter = shiftfs_read_iter,
.write_iter = shiftfs_write_iter,
.fsync = shiftfs_fsync,
.mmap = shiftfs_mmap,
.fallocate = shiftfs_fallocate,
.fadvise = shiftfs_fadvise,
.unlocked_ioctl = shiftfs_ioctl,
.compat_ioctl = shiftfs_compat_ioctl,
.copy_file_range = shiftfs_copy_file_range,
.remap_file_range = shiftfs_remap_file_range,
};
const struct file_operations shiftfs_dir_operations = {
.open = shiftfs_dir_open,
.release = shiftfs_dir_release,
.compat_ioctl = shiftfs_compat_ioctl,
.fsync = shiftfs_fsync,
.iterate_shared = shiftfs_iterate_shared,
.llseek = shiftfs_dir_llseek,
.read = generic_read_dir,
.unlocked_ioctl = shiftfs_ioctl,
};
여기서 타입별로 세팅하는 i_op
는 ctf문제들에 주로 나오는 디바이스 드라이버들의 fops
처럼 사용한다. 여기에 익숙한 ioctl
도 있다.
static long shiftfs_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
switch (cmd) {
case FS_IOC_GETVERSION:
/* fall through */
case FS_IOC_GETFLAGS:
/* fall through */
case FS_IOC_SETFLAGS:
break;
default:
if (!in_ioctl_whitelist(cmd, arg) ||
!shiftfs_passthrough_ioctls(file->f_path.dentry->d_sb->s_fs_info))
return -ENOTTY;
}
return shiftfs_real_ioctl(file, cmd, arg);
}
디렉토리와 파일 모두 같은 shiftfs_real_ioctl
함수를 사용한다.
static bool in_ioctl_whitelist(int flag, unsigned long arg)
{
void __user *argp = (void __user *)arg;
u64 flags = 0;
switch (flag) {
case BTRFS_IOC_FS_INFO:
return true;
case BTRFS_IOC_SNAP_CREATE:
return true;
case BTRFS_IOC_SNAP_CREATE_V2:
return true;
case BTRFS_IOC_SUBVOL_CREATE:
return true;
case BTRFS_IOC_SUBVOL_CREATE_V2:
return true;
case BTRFS_IOC_SUBVOL_GETFLAGS:
return true;
case BTRFS_IOC_SUBVOL_SETFLAGS:
if (copy_from_user(&flags, argp, sizeof(flags)))
return false;
if (flags & ~BTRFS_SUBVOL_RDONLY)
return false;
return true;
case BTRFS_IOC_SNAP_DESTROY:
return true;
}
return false;
}
사용하기 전에 in_ioctl_whitelist
함수에서 cmd
를 체크한다. 그리고 shiftfs_real_ioctl
를 사용한다.
Vulnerability
먼저 바로위의 in_ioctl_whitelist
에서 필터링된 cmd
를 거치고 shiftfs_real_ioctl
로 들어간다.
static long shiftfs_real_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
int newfd = -EBADF;
long err = 0, ret = 0;
void __user *argp = (void __user *)arg;
struct btrfs_ioctl_vol_args *btrfs_v1 = NULL;
struct btrfs_ioctl_vol_args_v2 *btrfs_v2 = NULL;
ret = shiftfs_btrfs_ioctl_fd_replace(cmd, argp, &btrfs_v1, &btrfs_v2, &newfd);
if (ret < 0)
return ret;
// here wrapper to vfs_ioctl()
// ...
err = shiftfs_btrfs_ioctl_fd_restore(cmd, newfd, argp, btrfs_v1, btrfs_v2);
if (!ret)
ret = err;
return ret;
}
그리고 필터링 된 cmd
를 파라미터로 shiftfs_btrfs_ioctl_fd_replace
함수로 들어가게 된다. 그리고 shiftfs_btrfs_ioctl_fd_restore
함수를 마지막에 호출한다.
static int shiftfs_btrfs_ioctl_fd_replace(int cmd, void __user *arg,
struct btrfs_ioctl_vol_args **b1,
struct btrfs_ioctl_vol_args_v2 **b2,
int *newfd)
{
struct btrfs_ioctl_vol_args *v1 = NULL;
...
if (cmd == BTRFS_IOC_SNAP_CREATE) {
v1 = memdup_user(arg, sizeof(*v1));
if (IS_ERR(v1))
return PTR_ERR(v1);
oldfd = v1->fd;
} else {
...
if (cmd == BTRFS_IOC_SNAP_CREATE) {
v1->fd = *newfd;
ret = copy_to_user(arg, v1, sizeof(*v1));
v1->fd = oldfd;
} else {
...
if (ret)
shiftfs_btrfs_ioctl_fd_restore(cmd, *newfd, arg, v1, v2);
return ret;
}
cmd
가 BTRFS_IOC_SNAP_CREATE
일때 유저스페이스의 구조체를 커널에 복사한다. 이 작업은 btrfs_ioctl_vol_args
에 포함된 fd
를 맨 아래 파일 시스템의 inode
의 fd
로 바꾸는 것이다.
그리고 fd
를 복사하는데 copy_to_user
를 사용하는데 여기서 copy_to_user
의 리턴값은 복사하고 남은 byte수이다.
Description
Copy data from kernel space to user space. Caller must check the specified block with access_ok before calling this function.
Returns number of bytes that could not be copied. On success, this will be zero.
성공할 경우에 0을 리턴하고 나머지 복사되지 않은 바이트를 출력. 그러므로 남은 데이터가 있으면 shiftfs_btrfs_ioctl_fd_restore
를 호출한다.
static int shiftfs_btrfs_ioctl_fd_restore(int cmd, int fd, void __user *arg,
struct btrfs_ioctl_vol_args *v1,
struct btrfs_ioctl_vol_args_v2 *v2)
{
int ret;
if (!is_btrfs_snap_ioctl(cmd))
return 0;
if (cmd == BTRFS_IOC_SNAP_CREATE)
ret = copy_to_user(arg, v1, sizeof(*v1));
else
ret = copy_to_user(arg, v2, sizeof(*v2));
__close_fd(current->files, fd);
kfree(v1);
kfree(v2);
return ret;
}
그리고 shiftfs_real_ioctl
에서 호출하는 shiftfs_btrfs_ioctl_fd_restore
함수를 여기서도 마지막에 호출한다. 이 함수는 커널에서 다시 유저의 영역으로 복사해준다.
#define BTRFS_PATH_NAME_MAX 4087
struct btrfs_ioctl_vol_args {
__s64 fd;
char name[BTRFS_PATH_NAME_MAX + 1];
};
복사하는 크기는 한 페이지고 4096 byte다.
여기서 가정은 shiftfs_btrfs_ioctl_fd_restore
함수를 두번 실행하게 되는 경우이다. 만약 이 함수를 동시에 두개 실행하게 된다면 같은 fd가 닫힌다.(__close_fd(current->files, fd);
) 그리고 같은 구조체를 두번 free
하게 된다. (kfree(v1); kfree(v2);
)
v1 = memdup_user(arg, sizeof(*v1));
v1
또는 v2
의 할당은 shiftfs_btrfs_ioctl_fd_replace
에서 memdup_user
함수로 한다.
/**
* memdup_user - duplicate memory region from user space
*
* @src: source address in user space
* @len: number of bytes to copy
*
* Return: an ERR_PTR() on failure. Result is physically
* contiguous, to be freed by kfree().
*/
void *memdup_user(const void __user *src, size_t len)
{
void *p;
p = kmalloc_track_caller(len, GFP_USER | __GFP_NOWARN);
if (!p)
return ERR_PTR(-ENOMEM);
if (copy_from_user(p, src, len)) {
kfree(p);
return ERR_PTR(-EFAULT);
}
return p;
}
EXPORT_SYMBOL(memdup_user);
이 커널함수 memdup_user
는 kmalloc
을 GFP_USER
플래그로 할당하는 함수이다. GFP_KERNEL
과 같은 slabs을 사용한다.
만약에 shiftfs_btrfs_ioctl_fd_replace
에서 copy_to_user
가 실패하여 ret
변수에 copy_to_user의 리턴값으로 나머지 값이 들어가서 shiftfs_btrfs_ioctl_fd_replace
에서 shiftfs_btrfs_ioctl_fd_restore
로 들어가게 된다면 이런 그림이 된다. v1 또는 v2값을 두번 해제하게 된다. 또한 할당한 것을 해제 안하는 것도 가능하다 (예를 들면 스프레이에 사용하는)
Exploit
double free버그는 두개의 해제 사이에 들어갈 할당과 컨트롤 할 수 있는 힙 청크가 있어야한다. 이런 경우에 userfaultfd
를 이용해 사이에서 사용자가 할당하는 방법을 커널익스에서 많이 사용한다.
userfaultfd
로 첫번째 할당이 끝난 뒤 free
된 청크에 같은 사이즈(4096 byte)로 컨트롤 할 수 있는 커널 힙 청크를 할당하고 실행하면 두번째 free
에서 내가 할당한 객체를 free시킬 것이다. 이 방법으로 익스를 진행한다.
익스플로잇 코드에서는 스레드를 이용해 스레드1에서는 copy_to_user
에서 userfaultfd
로 세팅한 메모리를 지정하고 해당 메모리로 커널이 접근하게 한다. 스레드2에서는 그 에러를 처리하며 힙 청크를 할당한다.
이제 어떤 컨트롤 가능한 힙 청크를 할당할지 정해야한다. 익스플로잇에서는 sysctl이라는 커널 파라미터를 변경하기 위한 기능을 이용해 aar/aaw를 만들었다.
static struct devinet_sysctl_table {
struct ctl_table_header *sysctl_header;
struct ctl_table devinet_vars[__IPV4_DEVCONF_MAX];
} devinet_sysctl = {
.devinet_vars = {
DEVINET_SYSCTL_COMPLEX_ENTRY(FORWARDING, "forwarding",
devinet_sysctl_forward),
DEVINET_SYSCTL_RO_ENTRY(MC_FORWARDING, "mc_forwarding"),
DEVINET_SYSCTL_RW_ENTRY(BC_FORWARDING, "bc_forwarding"),
...
net/ipv4/devinet.c
에 devinet_sysctl_table
이라는 구조체가 있다.
static int __devinet_sysctl_register(struct net *net, char *dev_name,
int ifindex, struct ipv4_devconf *p)
{
int i;
struct devinet_sysctl_table *t;
char path[sizeof("net/ipv4/conf/") + IFNAMSIZ];
t = kmemdup(&devinet_sysctl, sizeof(*t), GFP_KERNEL); // <--- Allocate
net/ipv4/conf
에 등록되는 sysctl
이다. 여기에서 힙 할당을 하게된다.
bash-1912 [002] .... 5515.306551: kmalloc: call_site=__devinet_sysctl_register+0x47/0x110 ptr=00000000552c19f5 bytes_req=2120 bytes_alloc=4096 gfp_flags=GFP_KERNEL
bash-1912 [002] .... 5515.306595: kmalloc: call_site=__devinet_sysctl_register+0x47/0x110 ptr=000000007dd47f0d bytes_req=2120 bytes_alloc=4096 gfp_flags=GFP_KERNEL
bash-1912 [002] .... 5515.306742: kmalloc: call_site=mr_table_alloc+0x42/0x100 ptr=00000000598ab799 bytes_req=3608 bytes_alloc=4096 gfp_flags=GFP_KERNEL|__GFP_ZERO
bash-1912 [002] .... 5515.306766: kmalloc: call_site=ipv4_mib_init_net+0xf4/0x1a0 ptr=00000000d0d71277 bytes_req=4096 bytes_alloc=4096 gfp_flags=GFP_KERNEL|__GFP_ZERO
bash-1912 [002] .... 5515.306811: kmalloc: call_site=__register_sysctl_table+0x50/0x1e0 ptr=000000001ae330cc bytes_req=3216 bytes_alloc=4096 gfp_flags=GFP_KERNEL|__GFP_ZERO
bash-1912 [002] .... 5515.306922: kmalloc: call_site=ipv6_init_mibs+0xb2/0x110 ptr=00000000dd7d83c1 bytes_req=4096 bytes_alloc=4096 gfp_flags=GFP_KERNEL|__GFP_ZERO
할당되는 이 구조체는 네트워크 네임스페이스를 생성하면 할당한다. 이 구조체에 있는 ctl_table
구조체에 포인터가 있는데 이를 이용해서 익스를 진행한다.
struct ctl_table {
const char * procname; /* 0 0x8 */
void * data; /* 0x8 0x8 */
int maxlen; /* 0x10 0x4 */
umode_t mode; /* 0x14 0x2 */
struct ctl_table * child; /* 0x18 0x8 */
proc_handler * proc_handler; /* 0x20 0x8 */
struct ctl_table_poll * poll; /* 0x28 0x8 */
void * extra1; /* 0x30 0x8 */
void * extra2; /* 0x38 0x8 */
};
위구조체(devinet_sysctl_table
)의 ctl_table
구조체 안을 보면 여기서 proc_handler
는 함수 포인터인데 이를 이용해 익스에 사용했다.
void collect_leak()
{
u64 *pu64 = (u64*)(gdata.alloc+1);
...
gdata.devinet_sysctl_forward_ptr = pu64[5];
gdata.table_header_addr = pu64[0];
}
먼저 릭 부분이다. 릭은 위에서 설명한 userfaultfd
에서 받은 free
된 메모리를 그냥 읽으면 devinet_sysctl_table
구조체가 할당되어 있다.
static struct devinet_sysctl_table {
struct ctl_table_header *sysctl_header;
struct ctl_table devinet_vars[__IPV4_DEVCONF_MAX];
} devinet_sysctl = {
.devinet_vars = {
DEVINET_SYSCTL_COMPLEX_ENTRY(FORWARDING, "forwarding",
devinet_sysctl_forward),
pu64[0]
이 sysctl_header
의 포인터이고 pu64[5]
가 ctl_table
배열의 첫번째 인자의 proc_handler
함수 포인터인 devinet_sysctl_forward
이다.
이 leak된 구조체를 이용하면 proc_handler
가 함수 포인터이기 때문에 pc
도 바꿀 수 있다. 익스에서는 aar/aaw를 이 구조체를 이용해 만들었다. sysctl
의 함수중 proc_doulongvec_minmax
함수를 이용해서 만들었다.
/**
* proc_doulongvec_minmax - read a vector of long integers with min/max values
* @table: the sysctl table
* @write: %TRUE if this is a write to the sysctl file
* @buffer: the user buffer
* @lenp: the size of the user buffer
* @ppos: file position
*
* Reads/writes up to table->maxlen/sizeof(unsigned long) unsigned long
* values from/to the user buffer, treated as an ASCII string.
*
* This routine will ensure the values are within the range specified by
* table->extra1 (min) and table->extra2 (max).
*
* Returns 0 on success.
*/
먼저 이 함수의 기능은 ctf_table
구조체의 extra1
(min), extra2
(max)를 이용해 ctf_table->data
에서 값을 읽거나 쓰는 기능이다.
sysctl
의 proc_handler
를 이 함수로 바꿔버리면 사용할 수 있다. aar/aaw를 만드는데 두개의 sysctl
을 사용한다. 첫번째 sysctl
로는 두번째 sysctl
의 ctl_table->data
를 덮어써 주소를 세팅하고 읽고 쓰는 방법이다.
이 과정에서 global sysctl
에 aar/aaw에 사용하는 ctl_table패치하는 과정이 따로 있는데 설명은 없지만 권한때문에 하는 것 같다.
여기까지 완료되면 aar/aaw와 pc
제어가 완료된다. 익스는 commit_cred(prepare_kernel_cred(0));
를 하던지 현재 프로세스의 task_struct
의 cred
를 덮던지 원하는 방식대로 사용 가능하다.
Patch
@@ -1491,8 +1495,18 @@ static int shiftfs_btrfs_ioctl_fd_replace(int cmd, void __user *arg,
v2->fd = oldfd;
}
- if (ret)
+ if (!ret) {
+ *b1 = v1;
+ *b2 = v2;
+ } else {
shiftfs_btrfs_ioctl_fd_restore(cmd, *newfd, arg, v1, v2);
+ }
+
+ return ret;
+
+err_free:
+ kfree(v1);
+ kfree(v2);
- return ret;
+ return ret ? -EFAULT: 0;
}
패치내용중 주요 내용은 copy_from_user
의 결과를 제대로 처리하도록 패치하였다.
Referenece
https://github.com/synacktiv/CVE-2021-3492/blob/master/exploit/main.c