It is not strictly necessary to call UnhookWindowsHookEx(), Windows does figure out that you forgot to do so and will unhook when your program terminates. Necessarily so, not cleaning up the hook would cause Windows to hang. It is considered "good manners" to not leave it up to the operating system.
The Application.Run() call is a hard requirement for a program that implements a low-level hook. Without a message loop, say using Console.ReadLine() instead, the callbacks to the hook are not made. The specific Windows promise is that the callback will be made on the same thread that called SetWindowsHookEx(). To do that, Windows somehow has the "break in" and force your thread to call the callback method. It can't just arbitrarily interrupt your thread and force it to make the call, that would cause horrible re-entrancy problems. Your thread has to be in a well defined state, it has to be idle and not mutating program state.
The message loop, specifically the GetMessage() or PeekMessage() winapi function, is that signal, when your thread pumps the message loop then it is idle and waiting for Windows to tell it to do something. It is the universal solution to the producer/consumer problem.