Monday, December 21, 2009

Remote Procedure Calls










Remote Procedure Calls


Remote Procedure Calls (RPC) allow applications to be designed and deployed in a distributed fashion by using a client/server architecture. Programmers can develop applications without worrying too much about the details of data encapsulation and transmission because the RPC interface handles these tasks automatically. There are two main RPC implementation and encoding standards: Open Network Computing (ONC) and Distributed Computing Environment (DCE). UNIX implements ONC-RPC (also known as Sun-RPC).


RPC applications are constructed by developing a server that exports a number of routines clients can call, provided they have adequate credentials. Each server program has a unique program number handed off to a special process known as portmap. When clients want to call a routine for an RPC server, several steps are involved:







1.
They connect to the portmapper service on a well-known port (UDP port 111 and TCP port 111).


2.
The client requests a specific service by supplying the unique program number associated with that service.


3.
Provided the service has been registered, portmap starts the service on an ephemeral port, and then reports back to the client with the port number the service is listening on.


4.
The client connects to the appropriate port, requests the routine it wants to call, and supplies arguments that the routine requires.



This description is a bit general. Some RPC servers have well-known ports (such as rpc.nfsd), and the interaction with portmap is sometimes unnecessary. Note that querying the portmap per service isn't necessarily required, and anyone who chooses to enforce access restrictions on clients by controlling access to the portmapper service isn't protecting the application.



RPC Definition Files


Most RPC applications implement their interfaces by using an RPC definition file (usually with .x as a file suffix). This file defines structures used throughout the program and the interface the server exports. The rpcgen tool on most modern UNIX systems can process these files and automatically generate client and server stub routines for communicating data between client and server applications. This tool takes a lot of the developer's work out of dealing with data transmission primitives in accordance with RPC design principles.


For code reviewers, this file is a convenient starting point for auditing RPC applications. You can quickly ascertain what functions are available to connecting clients and what arguments they take. The file format is quite straightforward. Developers can declare structures that the program is using as well as the RPC server interface (which is a structure definition). RPC abstracts the details of data transmission by using External Data Representation (XDR), a standard developed to represent data elements in a machine- and implementation-independent fashion. An RPC definition file that describes an RPC interface can represent arguments of different types. These types correspond directly to XDR basic types or structures composed of XDR basic types. The basic types in RPC definition files are as follows:


  • bool This is a Boolean value and can be in one of two states: true and false (nonzero and zero).

  • char This data type is identical to the char data type in C. As in C, characters can be signed or unsigned.

  • short This data type is the same as the C/C++ short data type. It can be signed or unsigned.

  • int An integer data type that's identical to the C/C++ int type and can be qualified with the unsigned keyword.

  • float Identical to the C/C++ float data type.

  • double Identical to the C/C++ double data type.

  • hyper The a 64-bit integer is the same as long long in C/C++.

  • string A string is a variable-length character array. Array definitions are described momentarily.

  • opaque Used to represent a byte stream of unspecified contents. It's much like the string type except that the RPC runtime doesn't NUL-terminate or attempt to interpret or decode it. Opaque data fields must be a fixed size.


In addition to basic data types, XDR allows the declaration of arrays and vectors. (Vectors are fixed-length arrays, so just the term "arrays" is used in this section.) Arrays are defined by using brackets (<>) with an optional size parameter. A fixed-length array looks like this:


int numberarray<1024>;


In this case, the RPC runtime ensures that an array supplied by a client doesn't exceed this maximum limit. Arrays can also be unbounded, as in this example:


int numberarray<>;


In this case, clients are free to supply any number of integers they choose. When used with the string and opaque types, the brackets indicate the length of the string, not an array of strings. A string with a maximum length of 255 bytes is declared like so:


string mystring<255>;


The server interface is defined by using the program keyword followed by the structure describing what routines have been exported. This structure can define multiple versions of the RPC program (using the version keyword), with each version exporting a unique set of procedures (although typically, they export the same ones). The prototype for an exported function is much like a C function prototype, with some differences; primarily, the function name is in uppercase letters and is followed by the procedure number assigned to that routine. Each routine that has been exported appears in the source code, but it's lowercase and has _svc appended to indicate it's a service routine. For example, you have the following declaration in the RPC definition file:


int HELLO_WORLD_1(void) = 1;


The server routine that implements it in the source is named


hello_world_1_svc().


Here's an example of a server definition. The following code fragment is from the sm_inter.x file, which defines the interface for the well-known rpc.statd service:


program SM_PROG {
version SM_VERS {
/* res_stat = stat_succ if status monitor agrees
to monitor */
/* res_stat = stat_fail if status monitor
cannot monitor */
/* if res_stat == stat_succ, state = state
number of site sm_name */
struct sm_stat_res SM_STAT(struct sm_name) = 1;

/* res_stat = stat_succ if status monitor agrees
to monitor */
/* res_stat = stat_fail if status monitor
cannot monitor */
/* stat consists of state number of local site */
struct sm_stat_res SM_MON(struct mon) = 2;

/* stat consists of state number of local site */
struct sm_stat SM_UNMON(struct mon_id) = 3;

/* stat consists of state number of local site */
struct sm_stat SM_UNMON_ALL(struct my_id) = 4;

void SM_SIMU_CRASH(void) = 5;

void SM_NOTIFY(struct stat_chge) = 6;

} = 1;
} = 100024;


The statd program has only one available version: version 1. It also exports six functions that clients can call remotely: sm_stat, sm_mon, sm_unmon, sm_unmon_all, sm_simu_crash, and sm_notify. To audit this application, an excellent starting point is looking for these functions in the source code because you know they're taking data from the client and processing it. You can also deduce what kind of data they're accepting from these prototypes; in the preceding example, they're specially defined structures, except sm_simu_crash, which doesn't take any arguments. To audit these functions, you can look up these structures to see what data you can supply. For example, if you want to audit the sm_stat function, you look for the definition of the sm_name structure, as shown:


const    SM_MAXSTRLEN = 1024;

struct sm_name {
string mon_name<SM_MAXSTRLEN>;
};


In this instance, you can supply a string that can be at most 1024 bytes. As you can see, RPC definition files allow you to quickly identify what code the server exposes to the client.




RPC Decoding Routines


The RPC definition file isn't required to create an RPC application. Developers might choose to hand-code the client and server stubs, which involves creating decoders for data manually by using the XDR routines exported for encoding and decoding. (Usually, the rpcgen tool uses XDR subroutines to encode structures and types defined in the RPC specification file.) XDR exports encoding and decoding routines for all its basic types: xdr_int(), xdr_string(), xdr_bool(), and so on. This lower-level manipulation introduces the opportunity for mistakes in the routines responsible for decoding data destined for certain routines. For example, the sm_name structure above has one element: a string with a maximum length of 1024. The XDR routine generated by rpcgen looks like this:


bool_t
xdr_sm_name(XDR *xdrs, sm_name *objp)
{
register int32_t *buf;

if(!xdr_string( xdrs, &objp->mon_name, SM_MAXSTRLEN))
return FALSE;
return TRUE;
}


If developers create these types of routines, they might accidentally use the wrong constants for maximum string lengths, not deal with errors properly, and so on. Therefore, when a developer doesn't use the RPC definition file, there's an additional lower layer where things might go wrong.


Note




Whether developers use the RPC definition file or not, there's a chance some implementations of rpcgen will make mistakes or the XDR libraries might have decoding errors. However, the system libraries usually aren't your primary concern when auditing an applicationbut they are well worth browsing in your spare time!






Authentication


RPC provides a number of authentication methods that can be used in applications that need to enforce access control for the functions they export:


  • AUTH_NONE When this method is selected, no authentication is required to use the RPC server; clients can call any routines they like. It's also referred to as AUTH_NULL in some implementations.

  • AUTH_UNIX Also commonly referred to as AUTH_SYS, with this authentication method, users provide a user ID, group ID list, and hostname indicating on which host they have the indicated privileges. For example, users connecting to an RPC server on host A might transmit credentials indicating they are the root user on host B. Because this mechanism relies on trust, it's totally unreliable. Indeed, this security is no better than no security enforcement because users can always transmit credentials indicating they are root (or any other user) on the local host where the RPC server resides. If you encounter a program that relies on this authentication mechanism, you have free access to any functions it provides.

  • AUTH_DES This method provides a more secure authentication mechanism that requires clients to verify their identity by encrypting a message with a private key (usually a timestamp). The server can use DES authentication to verify the client's identity, and the client can use DES to verify the server's identity.


RPC applications could possibly implement additional security features to help tighten control over applications, although additional features are used less often than they should be. If RPC authentication is in place, there's code to manually verify credentials in server routines or a dispatch function. In either case, some code is available to examine authentication data supplied with requests. It looks something like this:


int authenticate(struct svc_req *svc)
{
struct authunix_params *aup;
switch(rqstp->rq_cred.oa_flavor){
case AUTH_SYS:
aup = (struct authunix_params *)rqstp->rq_cred;

if(aup->aup_uid != 0)
return 1;
return 0;

default:
return 1;
}
}


This code has some verification of the requester's credentials, but it's using the AUTH_UNIX authentication method. As you know now, that method isn't much better than having no authentication at all.













No comments: