Ncrack engine A brief overview into Ncrack's architecture. ============================================ Ncrack is based on a modularized architecture, where each protocol/service corresponds to the equivalent module that handles all the authentication steps. Ncrack's architecture is thus built in a way so that a module is separated as much as possible from the more low level details of timing and connection management which are handled by the core engine. Ncrack utilizes the venerable nsock, a library written by Fyodor a long time ago and which has been refined and tested thoroughly throughout all the years of Nmap development. Nsock is a parallel sockets library which internally uses select(2) to poll through the registered socket descriptors and which upon a new network event (read/write/timeout etc) jumps to a preregistered callback handler which is responsible for doing something about that particular event. This is more or less an event-driven API. Ncrack's core engine resides in ncrack.cc which includes definitions for all these callback handlers: void ncrack_connect_handler(nsock_pool nsp, nsock_event nse, void *mydata); void ncrack_write_handler(nsock_pool nsp, nsock_event nse, void *mydata); void ncrack_read_handler(nsock_pool nsp, nsock_event nse, void *mydata); void ncrack_timer_handler(nsock_pool nsp, nsock_event nse, void *mydata); void ncrack_module_end(nsock_pool nsp, void *mydata); void ncrack_connection_end(nsock_pool nsp, void *mydata); After main() finishes parsing the user's host specifications and options it stores all the information it needs inside a ServiceGroup object (ServiceGroup.h). --- ServiceGroup / Service --- The ServiceGroup object holds all services (which are actually Service objects) that are going to be cracked. The Service (Service.h) class consists of variables that hold timing and statistical information, user-specified options that apply to that particular service/host, a pointer to a Target object (that is nearly the same as the known Target class from Nmap - only stripped of some unneeded fields like those that are related to network interfaces), functions that handle the username/password list iteration and a list of the active connections taking place at that moment. --- Connection --- A connection (class Connection currently residing in Service.h) is an instance that holds information pertaining to that particular TCP session between us and the service we are trying to crack. A connection must always belong to a Service class for obvious reasons. Usually, during a connection more than one authentication attempts are being carried out, depending on the service. --- module state machine --- The most important thing about a connection, is the 'state' it currently is in. The 'state' actually describes a specific step of the authentication procedure that is needed by the service we are cracking. Thus the number and names of states are defined in each module separately. For example, the authentication step, where at the beginning of a connection we need to wait and read the initial banner of the service, is specified by a particular 'state'. Another example, is the 'state' where we just need to write the password on the wire or the 'state' where in a service like telnet we have to make the option negotiation. It is pretty important that each 'state' performs a micro-action of the authentication procedure which will usually involve a certain nsock event to be registered. Of course, this might not be always possible to happen (see telnet). Ncrack core engine =================== The main nsock loop resides in function ncrack() @ ncrack.cc and it is responsible for polling for new events that result in calling back one of the registered handlers mentioned above. ncrack_probes() is called in the end of every loop iteration and checks if it can initiate new connections against any one of the targets in ServiceGroup. To understand how ncrack_probes() work, we first need to see the way that ServiceGroup handles its services lists. In the beginning, every user-specified service is stored inside the 'services_active' list. For every service residing there, we can automatically initiate a new connection at will. ServiceGroup keeps a lot more additional lists which hold services that for one reason or another cannot perform additional actions (like start a new connection) except for the connections having already started. For example, 'services_full' holds all services that cannot start another connection due to the fact that the total number of active connections taking place at that moment has reached the maximum allowed limit (connection limit). The list 'services_wait' keeps all services that need to wait a time of 'connection_delay' (usually specified by the user) before they can send another connection probe. The list 'services_finished' keeps all services that have been marked as finished, either because a critical error has occcured (like we got an RST at the first connection attempt or we kept getting too many timeouts for a prolonged time) or the username/password list iteration finished. The notion of keeping separate lists whose name imply the reason that the elements of the list are there, is also used (although to a lesser and simpler extent) by Nmap's service scanning engine. --- ncrack_probes() --- ncrack_probes() iterates the ServiceGroup 'services_active' list, initiating new connections until every service has been moved inside a different ServiceGroup list. Note that it doesn't wait for any connection to actually finish the 3way handshake, since nsock uses non-blocking connect(2)s and ncrack only needs to register the event and the callback handler (ncrack_connect_handler). --- ncrack_connect_handler() --- Upon connection success ncrack_connect_handler() gives control to call_module() which calls the service module function corresponding to the particular service this connection is up against. If the connection times out or we get an RST and this is our first connection attempt, then we mark the service as 'dead' moving it to 'services_finished' list. This is particularly useful when the user specifies the targets in a wildmask or netmask notation, blindly searching for services to crack. It is very probable that some hosts will not even have that service listening and thus we will cease trying to crack them. It is important to note that this first connection probe (boolean 'just_started' @ Service class) also collects valuable timing information like how many authentication attempts the server allows to make per connection. That is why ncrack doesn't open more than 1 connection probes against a service before that first timing probe finishes its job (which will entail exhausting all allowed authentication attempts during that connection). --- ncrack_write_handler() --- The write handler is probably the simplest one. The only thing it needs to do is check the nsock return status and report on us in case of error. The case of a write failing is the most improbable one. It can happen though in case we write on a closed socket (which won't normally happen since we always check if the socket is still active or the peer closed on us) or if the kernel's socket buffers are full (which can only occur on very old systems with a small amount of RAM). --- ncrack_read_handler() --- The read handler is responsible for filling in the Connection's auxiliary buffer upon a successful nsock read event. We also use a trick to check whether or not the peer is still active or it has sent us a FIN closing the connection. Whenever the boolean 'check_closed' is true, if nsock produces a TIMEOUT instead of an EOF error, then it means we are still online. This happens because the caller that wants to check the connection state, registers a read event with a very small timeout. This is a hack that allows us to check in a portable way if we have moved to the CLOSE_WAIT state from ESTABLISHED. --- ncrack_module_end() --- This functions should be called by a module whenever it knows that it has completely finished an authentication attempt. It updates statistical variables for the service, like how many attempts in total have been made and also currently implements part of the dynamic timing engine. Every 500 msecs it checks whether the current authentication rate is less than the last calculated one and takes appropriate steps to increase it. Since the 'ideal_parallelism' variable which is the dominating connection metric can change, we also check if we can move our service from 'services_full' to 'services_active' and call ncrack_probes() to potentially initiate new connections. Finally, if we need to check if our peer is alive (variable 'peer_alive' is false), we do the read timeout trick mentioned above. --- ncrack_connection_end() --- One of the most complex functions. It takes all necessary actions whenever a connection is ended - either normally or by an error. Firstly, it checks if we received a FIN from our peer, in which case one of the following could have happened: i) The peer might have closed on us 'unexpectedly': this happens with services like telnet that can close the connection immediately after giving the final results of the last authentication attempt. For services like these we need to always set the variable 'peer_might_close' inside the module immediately after the state that is responsible for writing the password on the wire and before the state that registers the next read call. If we are the first 'timing' probe then we increase the number of supported authentication attempts per connection for this service. ii) The peer might have closed on us normally in which case we don't do anything. iii) The peer might have closed on us in the middle of the authentication. This shouldn't normally happen and it is an indication of a really strange error, usually due to extreme network conditions. If the above or just a timeout in the middle of the authentication happens, then we adapt the dynamic timing engine to drop the 'ideal_parallelism' limit. Next, if we are the first timing probe, depending on the timing template, we calculate our initial ideal parallelism. We also update the authentication rate meters accordingly. In the end of the function we also call ncrack_probes() since we might have changed 'ideal_parallelism'.