--- [ Ncrack engine ] ---
A brief overview of 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 which was written by Fyodor a long time
ago and 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 (Connection.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 going to be 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 function 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'.
|