编程安全 Linux系统为程序员提供了许多系统调用和C程序库,这些系统调用可存取各种安全属性。有些是信息调用,返回文件属性,实际的和有效的UID、GID等信息。有些可改变文件属性。UID、GID等有些处理口令文件和小组文件,还有些完成加密和解密。
以root权限运行的程序可以做很多事,很明显,系统安全不仅仅是系统管理员的责任,程序员也需要注意有关系统调用,标准C库子程序的安全,写出安全的C程序很重要。
一般有两方面的安全问题,在写程序时必须考虑:
A>确保自己建立的任何临时文件不含有机密数据,如果有机密数据,设置临时文件仅对自己可读/写。确保建立临时文件的目录仅对自己可写。 B>确保自己要运行的任何命令(通过system()、popen()、execlp()、execvp()运行的命令)的确是自己要运行的命令,而不是其他什么命令,尤其是自己的程序为SUID或SGID许可时要小心。
第一方面比较简单,在程序开始前调用umask(077)。若要使文件对其他人可读,可再调 chmod(),也可用下述语名建立一个“不可见”的临时文件。 creat("/tmp/xxx",0); file=open("/tmp/xxx",O_RDWR); unlink("/tmp/xxx");
文件/tmp/xxx建立后,打开,然后断开链,但是分配给该文件的存储器并未删除,直到最终指向该文件的文件通道被关闭时才被删除。打开该文件的进程和它的任何子进程都可存取这个临时文件,而其他进程不能存取该文件,因为它在/tmp中的目录项已被unlink()删除。
第二方面比较复杂而微妙,由于system()、popen()、execlp()、execvp()执行时,若不给出执行命令的全路径,就能“骗”用户的程序去执行不同的命令。因为系统子程序是根据PATH变量确定哪种顺序搜索哪些目录,以寻找指定的命令,这称为SUID陷井。最安全的办法是在调用system()前将有效UID改变成实际UID,另一种比较好的方法是以全路径名命令作为参数。execl()、execv()、execle()、execve()都要求全路径名作为参数。对付SUID陷井的另一方式是在程序中设置PATH,由于system()和popen()都启动外壳,故可使用外壳句法。如:system("PATH=/bin:/usr/bincd");这样允许用户运行系统命令而不必知道要执行的命令在哪个目录中,但这种方法不能用于execlp()和execvp()中,因为它们不能启动外壳执行调用序列传递的命令字符串。关于外壳解释传递给system()和popen()的命令行的方式,有两个其他的问题:.外壳使用IFS外壳变量中的字符,将命令行分解成单词(通常这个外壳变量中是空格、tab、换行),如IFS中是/,字符串/bin/ed被解释成单词bin,接下来是单词ed,从而引起命令行的曲解。
再强调一次:在通过自己的程序运行另一个程序前,应将有效UID改为实际的UID,等另一个程序退出后,再将有效UID改回原来的有效UID。
系统调用
- I/O
- 进程控制
- 文件属性
- UID和GID的处理
标准C库
- I/O
- /etc/passwd的处理
- /etc/group的处理
- 加密子程序
- 运行shell
系统调用
I/O
进程控制
文件属性
UID和GID的处理
I/O
.creat() 建立一个新文件或重写一个暂存文件。需要两个参数:文件名和存取许可值(8进制方式)。如:creat(“/usr/pat/read_write”,0666)/*建立存取许可方式为0666的文件*/调用此子程序的进程必须要有建立的文件的所在目录的写和执行许可,置给creat()的许可方式变量将被umask()设置的文件建立屏蔽值所修改,新文件的所有者和小组由有效的UID和GID决定。返回值为新建文件的文件描述符。 . fstat() 见后面的stat()。
.open() 在C程序内部打开文件。需要两个参数:文件路径名和打开方式(I、O、I&O)。如果调用此子程序的进程没有对于要打开的文件的正确存取许可(包括文件路径上所有目录分量的搜索许可),将会引起执行失败。如果此子程序被调用去打开不存在的文件,除非设置了O_CREAT标志,调用将不成功。此时,新文件的存取许可将作为第三个参数(可被用户的umask修改)。当文件被进程打开后再改变该文件或该文件所在目录的存取许可,不影响对该文件的I/O操作。
.read() 从已由open()打开并用作输入的文件中读信息。它并不关心该文件的存取许可。一旦文件作为输入打开,即可从该文件中读取信息。
.write() 输出信息到已由open()打开并用作输出的文件中。它同read()一样也不关心该文件的存取许可。
进程控制
1.exec()族 包括execl()、execv()、execle()、execve()、execlp()和execvp(),可将一可执行模快拷贝到调用进程占有的存贮空间。正被调用进程执行的程序将不复存在,新程序取代其位置。这是Linux系统中一个程序被执行的唯一方式:用将执行的程序复盖原有的程序。 安全注意事项: .实际的和有效的UID和GID传递给由exec()调入的不具有SUID和SGID许可的程序。 .如果由exec()调入的程序有SUID和SGID许可,则有效的UID和GID将设置给该程序的 所有者或小组。 .文件建立屏蔽值将传递给新程序。 .除设了对exec()关闭标志的文件外,所有打开的文件都传递给新程序。用fcntl()子 程序可设置对exec()的关闭标志。
2.fork() 用来建立新进程。其建立的子进程是与调用fork()的进程(父进程)完全相同的拷贝(除了进程号外)。 安全注意事项: .子进程将继承父进程的实际和有效的UID和GID。 .子进程继承文件方式建立屏蔽值。 .所有打开的文件传给子进程。
3.signal() 允许进程处理可能发生的意外事件和中断。需要两个参数:信号编号和信号发生时要调用的子程序。信号编号定义在signal.h中。信号发生时要调用的子程序可由用户编写,也可用系统给的值,如:SIG_IGN则信号将被忽略,SIG_DFL则信号将按系统的缺省方式处理。如许多与安全有关的程序禁止终端发中断信息(BREAK和DELETE),以免自己被用户终端终止运行。有些信号使Linux系统的产生进程的核心转储(进程接收到信号时所占内存的内容,有时含有重要信息),此系统子程序可用于禁止核心转储。
文件属性
1.access() 检测指定文件的存取能力是否符合指定的存取类型。需要两个参数:文件名和要检测的存 取类型(整数)。 存取类型定义如下: 0:检查文件是否存在。 1:检查是否可执行(搜索)。 2:检查是否可写。 3:检查是否可写和执行。 4:检查是否可读。 5:检查是否可读和执行。 6:检查是否可读可写可执行。 这些数字的意义和chmod命令中规定许可方式的数字意义相同。此子程序使用实际的UID 和GID检测文件的存取能力(一般有效的UID和GID用于检查文件存取能力)。 返回值:0:许可-1:不许可。
2.chmod() 将指定文件或目录的存取许可方式改成新的许可方式。 需要两个参数:文件名和新的存取许可方式。
3.chown() 同时改变指定文件的所有者和小组的UID和GID。(与chown命令不同。) 由于此子程序同时改变文件的所有者和小组,故必须取消所操作文件的SUID和SGID许可, 以防止用户建立SUID和SGID程序,然后运行chown()去获得别人的权限。
4.stat() 返回文件的状态(属性)。需要两个参数:文件路径名和一个结构指针,指向状态信息的 存放的位置。 结构定义如下: st_mode:文件类型和存取许可方式。 st_ino:I节点号。 st_dev:文件所在设备的ID。 st_rdev:特别文件的ID。 st_nlink:文件链接数。 st_uid:文件所有者的UID。 st_gid:文件小组的GID。 st_size:按字节计数的文件大小。 st_atime:最后存取时间(读)。 st_mtime:最后修改时间(写)和最后状态的改变。 st_ctime:最后的状态修改时间。 返回值:0:成功1:失败。
5.umask() 将调用进程及其子进程的文件建立屏蔽值设置为指定的存取许可。 需要一个参数:新的文件建立屏值。
UID和GID的处理
1)getuid()返回进程的实际UID。
2)getgid()返回进程的实际GID。
以上两个系统调用可用于确定是谁在运行进程。
3)geteuid()返回进程的有效UID。
4)getegid()返回进程的有效GID。
以上两个系统调用用于确定某程序是否在运行某用户而不是其他用户的SUID程序时很有用,可调用它们来检查确认本程序的确是以该用户的SUID许可在运行。
5)setuid():用于改变有效的UID。
对于一般用户,此子程序仅对要在有效和实际的UID之间变换的SUID程序才有用(从原有效UID变换为实际UID),以保护进程不受到安全危害。实际上该进程不再使用SUID方式运行。
6)setgid()用于改变有效的GID。
标准C库
I/O
/etc/passwd的处理
/etc/group的处理
加密子程序
运行shell
I/O
1.fopen() 打开一个文件供读或写,安全方面的考虑同open()一样。
2.fread()、getc()、fgetc()、gets()、scanf()和fscanf() 从已由fopen()打开供读的文件中读取信息。它们并不关心文件的存取许可。这一点同 read()。
3.fwrite()、put()、fputc()、puts,fputs()、printf()、fprintf() 把信息写到已由fopen()打开供写的文件中。它们也不关心文件的存取许可。这一点同 write()。
4.getpass() 从终端上读取至多8个字符长的口令,不回显用户输入的字符。 需要一个参数:提示信息。该子程序将提示信息显示在终端上,禁止字符回显功能,从 /dev/tty读取口令,然后再恢复字符回显功能,返回刚敲入的口令的指针。
/etc/passwd的处理
有一组子程序可对/etc/passwd文件进行方便地存取,可在入口项对文件读取、写入或更新 等等。 1.getpwuid() 从/etc/passwd文件中获取指定的UID的入口项。 2.getpwnam() 对于指定的登录名,在/etc/passwd文件检索入口项。以上两个子程序返回一个指向passwd 结构的指针,该结构定义在/usr/include/pwd.h中,定义如下: structpasswd{ char*pw_name;/*登录名*/ char*pw_passwd;/*加密后的口令*/ uid_tpw_uid;/*UID*/ gid_tpw_gid;/*GID*/ char*pw_age;/*代理信息*/ char*pw_comment;/*注释*/ char*pw_gecos; char*pw_dir;/*主目录*/ char*pw_shell;/*使用的外壳*/ }; 3.getpwent()、setpwent()、endpwent() 对口令文件作后续处理。 首次调用getpwent(),打开/etc/passwd并返回指向文件中第一个入口项的指针,保持调用 之间文件的打开状态。再调用getpwent()可顺序地返回口令文件中的各入口项。调用 setpwent()把口令文件的指针重新置为文件的开始处。使用完口令文件后调用endpwent() 关闭口令文件。 4.putpwent() 修改或增加/etc/passwd文件中的入口项。 此子程序将入口项写到一个指定的文件中,一般是一个临时文件,直接写口令文件是很危 险的。最好在执行前做文件封锁,使两个程序不能同时写一个文件。算法如下: .建立一个独立的临时文件,即/etc/passnnn,nnn是PID号。 建立新产生的临时文件和标准临时文件/etc/ptmp的链,若建链失败,则为有人正在使用 /etc/ptmp,等待,直到/etc/ptmp可用为止或退出。 .将/etc/passwd拷贝到/etc/ptmp,可对此文件做修改。 .将/etc/passwd移到备份文件/etc/opasswd。 .建立/etc/ptmp和/etc/passwd的链。 .断开/etc/passnnn与/etc/ptmp的链。 注意临时文件应建立在/etc目录,才能保证文件处于同一文件系统中,建链才能成功, 且临时文件不会不安全。此外,若新文件已存在,即便建链的是root用户,也将失败, 从而保证了一旦临时文件成功地建链后没有人能再插进来干扰。当然,使用临时文件 的程序应确保清除所有临时文件,正确地捕捉信号。
/etc/group的处理
有一组类似于前面的子程序处理/etc/group的信息,使用时必须用include语句将/usr/include/grp.h文件加入到自己的程序中。该文件定义了group结构,将由getgrnam()、 getgrgid()、getgrent()返回group结构指针。 1.getgrnam() 在/etc/group文件中搜索指定的小组名,然后返回指向小组入口项的指针。 2.getgrgid() 类似于前一子程序,不同的是搜索指定的GID。 3.getgrent() 返回group文件中的下一个入口项。 4.setgrent() 将group文件的文件指针恢复到文件的起点。 5.endgrent() 用于完成工作后,关闭group文件。 6.getuid() 返回调用进程的实际UID。 7.getpruid() 以getuid()返回的实际UID为参数,确定与实际UID相应的登录名,或指定一个UID为参 8.getlogin() 返回在终端上登录的用户的指针。 系统依次检查STDIN、STDOUT、STDERR是否与终端相联,与终端相联的标准输入用于 确定终端名,终端名用于查找列于/etc/utmp文件中的用户,该文件由login维护,由who程序用 来确认用户。 9.cuserid() 首先调用getlogin(),若getlogin()返回NULL指针,再调用getpwuid(getuid())。 10.logname 列出登录进终端的用户名。 11. whoamI 显示出运行这条命令的用户的登录名。 12. id 显示实际的U I D和G I D(若有效的U I D和G I D和实际的不同时也显示有效的U I D和G I D)和 相应的登录名。
加密子程序 1977年1月,NBS宣布一个用于美国联邦政府ADP系统的网络的标准加密法:数据加密标准即DES用于非机密应用方面。DES一次处理64BITS的块,56位的加密键。
1.setkey()和encrypt() 提供用户对DES的存取。 这两个子程序都取64bits长的字符数组,数组中的每个元素代表一个位,为0或1。 setkey()设置将按DES处理的加密键,忽略每第8位构成一个56位的加密键。 encrypt()然后加密或解密给定的64bits长的一块,加密或解密取决于该子程序的第二个变元,0:加密1:解密。
2.crypt() 是Linux系统中的口令加密程序,也被/usr/lib/makekey命令调用。 crypt()子程序与crypt命令无关,它与/usr/lib/makekey一样取8个字符长的关键词,2个salt字符。关键词送给setkey(),salt字符用于混合encrypt()中的DES算法,最终调用encrypt()重复25次,加密一个相同的字符串。返回加密后的字符串指针。
运行shell 1.system() 运行/bin/sh执行其参数指定的命令,当指定命令完成时返回。 2.popen() 类似于system(),不同的是命令运行时,其标准输入或输出联到由popen()返回的文件指针。
二者都调用fork()、exec()、popen()还调用pipe(),完成各自的工作,因而fork()和exec()的安全方面的考虑开始起作用。
|