Security-specific Programming Errors (Part 6)
Thomas Biege
Table of Contents
System Limits (not limited to C!)
There are different system resources that administrators and programmers can set limits for by using ulimit(1) or setrlimit(2), respectively. By limiting file descriptors or memory sizes, for example, it is possible to prevent so-called denial-of-service attacks. If the core file size is set to 0, you can even prevent unintentional disclosure of information. (Core files contain a copy of a program's memory shortly before it crashed). For example, there was an FTP daemon bug in the Solaris operating system a few years ago. The FTP server loaded the content of the /etc/shadow file into memory in order to authenticate the user. Regular users are not authorized to read this file. An attacker was able to send signals that caused the process to abort and to write a core file. The attacker could then use the core file to extract the data from the protected file /etc/shadow.
Example for setting system limits:
[...]
extern int setlimits(sl_limit slim)
{
struct rlimit rst;
rst.rlim_cur = 0;
rst.rlim_max = slim.fsize;
if(setrlimit(RLIMIT_FSIZE, &rst) < 0)
return(-1);
rst.rlim_max = slim.data;
if(setrlimit(RLIMIT_DATA, &rst) < 0)
return(-1);
rst.rlim_max = slim.stack;
if(setrlimit(RLIMIT_STACK, &rst) < 0)
return(-1);
rst.rlim_max = slim.core;
if(setrlimit(RLIMIT_CORE, &rst) < 0)
return(-1);
rst.rlim_max = slim.rss;
if(setrlimit(RLIMIT_RSS, &rst) < 0)
return(-1);
rst.rlim_max = slim.nofile;
if(setrlimit(RLIMIT_NOFILE, &rst) < 0)
return(-1);
rst.rlim_max = slim.nproc;
if(setrlimit(RLIMIT_NPROC, &rst) < 0)
return(-1);
rst.rlim_max = slim.memlock;
if(setrlimit(RLIMIT_MEMLOCK, &rst) < 0)
return(-1);
return(0);
}
[...]
An old version of su(1) serves as a negative example for setting system limits. su(1) attempted to open the password file. If this attempt failed, the system opened a command shell with root privileges without prompting the user for a password, since su(1) assumed that there was a problem with the file system. If a malicious user set the maximum number of open file descriptors to 0, he was able to outwit su(1) to gain root access. Whenever errors occur, security-specific programs should terminate and not try to interpret and fix the error.
Signals (not limited to C!)
A programmer should know quite a bit about this topic, so that his programs are stable, run securely and cannot be interrupted by sending signals.
The following conditions must be met in order for a process to accept signals from another process (BSD kernel): (Sender S, recipient R)
- the real UID for S is the same as for root
- the real UID is the same for S and R
- the effective UID is the same for S and R
- the real UID for S is the same as the effective UID for R
- the effective UID for S is the same as the real UID for R
- S and R have the same login session ID
You can use setuid(2) or seteuid(2) to change the UID and you can use setsid(2) to change the session ID.
Before processing critical sections of code, a program should always block all signals. Your own signals also pose a danger, that is, those signals that were not sent by a user.
For example:
[...]
extern int sigprotection(u_int toggle, sigset_t *sp_blockmask)
{
static sigset_t sp_savedmask;
static u_int sp_status = SP_OFF;
switch(toggle)
{
case SP_ON:
if(sp_status != SP_ON)
{
if(sp_blockmask == NULL)
return(-1);
sp_status = SP_ON;
if(sigprocmask(SIG_BLOCK, sp_blockmask,
&sp_savedmask) < 0)
return(-1);
}
break;
case SP_OFF:
if(sp_status != SP_OFF)
{
sp_status = SP_OFF;
if(sigprocmask(SIG_SETMASK, &sp_savedmask,
NULL) < 0)
return(-1);
}
break;
default:
return(-1);
}
return(0);
}
[...]
A bug that occurred in the ping program serves to illustrate the relevance of signals. Ping sends time-delayed ICMP echo request messages through the network to determine the existence and availability of another network component. Thus, an attacker was able to send the SIGALRM signal at short intervals and practically compensate for the time delay in order to flood the network with ICMP packets.
Interval Timers
There are three interval timers:
| ITIMER_REAL |
measures the time that is actually elapsing and returns SIGALRM after reaching zero. |
| ITIMER_VIRTUAL |
only measures when the process is being executed and returns SIGVTALRM after reaching zero. |
| ITIMER_PROF |
measures the computing time required by the process, as well as the time period during which the system is working for the process. Thus, in conjunction with ITIMER_VIRTUAL, it is possible to determine how long an application is running in the user and kernel program areas. Once the counter is finished, the system returns SIGPROF. |
Since the child process inherits these timers, security-specific programs should ignore these timers so that the programs are not interrupted by unwanted signals.
You can use setitimer(2) and getitimer(2) to manipulate and request the interval timers.
Terminals and Escape Sequences (not limited to C!)
Terminals can display not just characters, but also provide an option to use escape sequences to position the cursor, change foreground and background colors, redefine keys or even execute commands. This is true not just for the older video terminals (VTs) connected to mainframes with serial interfaces, but also for the corresponding emulators required to run command shells.
An attacker can exploit these properties to render the terminal unusable, forge information or even execute commands.
To neutralize this threat, programs such as mail clients, chat programs, Web browsers, etc. that write to a user's terminal should filter out the escape character. (The escape character for the vtXXX terminal is: 0x00 through 0x1F and 0x7F through 0x9F). An even better solution is to only allow letters, numbers and punctuation marks.
Handling Sensitive Data (not limited to C!)
It is best to delete sensitive data, such as passwords, personal information, etc., from RAM immediately after use. Ideally, this data should be processed by a small process - that is, a process that is easy to audit - and a process that uses only secure IPC (e.g., pipes) to communicate with the user program and one that is completely self-sufficient (i.e., a process with its own UID, GID, session ID). We recommend the use of encryption in certain extreme cases.
Buffered I/O functions are often used for sensitive data. This may have been the application programmer's intent or it may be part of the library that is not affected by the programmer. In such cases, sensitive information is stored in areas that cannot be easily deleted. Here you should use setbuf(3) to delete the buffer for the stream by activating setbuf(fstream, NULL).
If you want to securely overwrite data on the hard drive (or in RAM), we recommend that you read Peter Gutmann's paper (Peter Gutmann; Secure Deletion of Data from Magnetic and Solid-State Memory; USENIX 6) . Secure deletion of data means that it cannot be restored even with magnetic force microscopy. However, every time you overwrite data, you should activate fflush(3) and then sync(2) to physically write the data to the medium You should not use fsync(3). In contrast to sync(2), fsync(3) does not block, which may cause the system to be inconsistent when physically writing the special bitmaps.
Miscellaneous
Generally, there aren't many things that you can do wrong while creating links. However, you should make sure to always provide absolute path name information for the shared libraries. You can use relative path names to shared libraries to load your own libraries at run-time. Thus, privileged processes would violate security measures, since they would execute functions created by the attacker.
For privileged applications and network services, it is essential that you avoid using command shell calls such as system(3) or popen(3) (as well as getcwd(3) under the old SunOS, as mentioned above). Even if the user input was filtered, undesirable interactions with the shell
are still possible, such as, for example with the environment variables ENV and
BASH_ENV (see bash(1)).
|