读大牛Ulrich Drepper 关于写安全的代码的心得及记录。
关键点
Section 2 Safe Programming
memory的边界1.2 预防基于文件系统的攻击
提供了一个宏,来更好的防止调用malloc的指针错误
#define alloc(type) \ (type) malloc(sizeof(*(type)))
与其它文章一样,解释了为什么不要使用gets.getwd. 它们都没有对输入进行check, 容易溢出.
隐式的内存分配
标准库不断的引入了一些更安全的接口, 希望大家都使用新的接口, 因为它们更好的关注了内存溢出的问题.
比如, asprintf,vsprintf就比snprintf安全. 推荐大家使用open_memstream这个接口,
同时,也推荐大家使用ssize_t getdelim(char **lineptr, size_t *n, int delim, FILE *stream); ssize_t getline(char **lineptr, size_t *n, FILE *stream);
1.3 降低风险
综述了类Unix系统的关于文件的基本机制:一个文件的内容可以在文件系统中存在对应的一个或多个名字. 也就是说一个文件可以没有与之对应的命名而存在,只需要有与之对应的一个文件描述符即可.
这就要求程序员们能时候牢记: 文件名和文件内容之间的关系在任意时候都可能被改变.
打开文件的时候进行检查
这里要注意顺序, 先打开文件描述符, 再对其进行检查. 否则, 如果相反的话, 有可能就会使得攻击者有在打开之前改变对应的文件名与文件内容对应关系的机会.
fd = open(filename, O_RDWR); if (fd != -1 && fstat (fd, &st) != -1 //使用fstat而不是stat,可以确保我们使用的是文件描述符而不是文件名来获得文件信息 && st.st_ino == ino && st.st_dev == dev) { ... } close (fd);这里关键的注意点: 使用文件名来调用某些函数,从而获得文件信息并用以进行检查的做法, 是不安全的. Ulrich Drepper 给了以下的一个列表, 请大家使用的时候多留意.
这里需要注意下chdir(), 它虽然不是操作文件,而是对目录进行操作, 但是, 由于有symbolic link的存在, 使用它的时候尤其要注意.
因为入侵者可能会利用symbolic link, 来使得用户在不知情的情况下,操作一个入侵者提供的目录, 尤其是这个目录是可以被任意人读写操作的目录, 如/tmp.
提供了两种预防手段:
方法一:
int safe_chdir(const char *name) { int dfd = open(name,O_RDONLY|O_DIRECTORY|O_NOFOLLOW); //先用open的一些参数,确保打开的是严格的目录 if (dfd == -1) return -1; int ret = fchdir(dfd); //接着再用fchdir打开它 close(dfd); return ret; }
这里的O_NOFOLLOW的使用需要注意, 这有可能对系统的可用性带来影响(它限制了symbolic link在文件系统中的作用域). 使用前,请确认你所操作的文件, 是否可以使用它.
方法二: 当需要处理"..""目录时
int dfd = open(".", O_RDONLY); // 先打开当前目录 int ret_workdir = chdir("12b"); // 11->12b->..->11, 这里如果不这样做, 如果入侵者可以为12b做一个symbolic link, 则有可能..返回的是12b所指向的内容 int ret = fchdir(dfd); // 在使用该目录的文件描述符切换目录, 而不是使用chdir("..")返回上一层close(dfd);文中给出了上面的解决方案在linux上可能遇到问题的原因: 有些目录只有execute权限(没有read/write权限), 这样设置是为了只有owner(它才能知道该目录下的文件的全路径,这样能提供一定的安全性)才能 使用fchdir转换到该目录. 同时,如果系统提供了nftw()之类的接口, 可以使用该函数, 因为这个函数工作在它内部的数据结构中, 可以防止遍历并操作的中途, 被其它的入侵者打断. 介绍了fexecve()接口 优点: 可以对binary文件进行安全的check.如, 打开一个文件, 然后用该文件句柄调用checksum进行计算, 验证过checksum之后, 再调用fexecve执行. 缺点: 需要额外的读权限; 该接口的实现通常实在userland上通过proc文件系统完成, 需要执行的文件所在的文件系统是mount上,否则无法执行. getxattr(), setxattr(), listxattr()与第一条介绍的stat()类似安全的创建文件 创建文件需要关注的问题是: 不会无意的覆盖, 破坏之前的文件. 创建临时文件更需要注意. 对于需要创建文件的程序, 应该先创建一个临时文件(比如放在/tmp, 它在重启后会自动被清空), 再所有的初始化及操作结束之后再把它放入正真的工作目录. 这样可以保证在操作被打断之后, 不许要做重新初始化, 删除不完整文件的动作. 请使用nkstemp()来创建临时文件. 它可以在工作目录创建临时文件, 并保证不会重复. 如果可以操作/tmp, 那么也可以使用tmpfile()来代替, 这是最好的选择. 同样在创建临时文件时,通常需要创建临时目录, 那么请使用 mkdtemp().正确的重命名, 替换, 移除 使用以下接口时, 请注意避免破坏有用的数据并处理好冲突.int unlink(const char *pathname); int rmdir(const char *pathname); int remove(const char *pathname); int rename(const char *oldpath, const char *newpath);int link(const char *oldpath, const char *newpath);有问题的代码: 这里在rename的之前, 如果正好有多个进程通过了fstat, 那么在rename的时候就容易造成一个进程的数据覆盖了另外一个.int fd = open(finalpath, O_RDWR); if (fd == -1 || fstat(fd, &st) == -1 || extratests(fd, &st) != 0) { fd = mkstemp(tmppath); // initialize file FD ... rename(tmppath, finalpath); }应该避免使用rename
int fd; while (1) { fd = open(finalpath, O_RDWR); if (fd != -1 && fstat(fd, &st) != -1 && extratests(fd, &st) == 0) break; if (fd != -1) { // the file is not usablechar buf[40]; sprintf(buf, "/proc/self/fd/%d", fd); // 这里使用proc文件系统来判断该文件描述符确实被目前的进程所操作!!! unlink(buf); close(fd); } fd = mkstemp(tmppath); // initialize file FD ... if (link(tmppath, finalpath) == 0) { unlink(tmppath); break; } close(fd); unlink(tmppath); }
或者 也可以如下来确保我们所要rename的文件没有被篡改:
int frename(int fd, const char *newname) { char buf[40]; sprintf(buf, "/proc/self/fd/%d", fd); size_t nbuf2 = 200; char *buf2 = alloca(nbuf2); int n; while ((n = readlink(buf, buf2, nbuf2)) == nbuf2) buf2 = alloca(nbuf2 *= 2); buf2[n] = ’\0’; static const char deleted[] = " (deleted)"; if (n < sizeof(deleted) || memcmp(buf2 + n - (sizeof(deleted) - 1), deleted, sizeof(deleted) - 1) != 0) return rename(buf2, newname); errno = ENOENT; return -1; }
尽量限制一个程序所要的特权.1.4 不要轻易相信任何人
一个程序可以要求以下的特权:
内存访问保护, 特别是在IA-32上的Exec-Shield.
注意由mmap, mprotect所管理的内存的权限, 尽量限制对内存操作的权限. 这需要在设计时就有所考量.文件访问保护
同样,尽可能限制对一个文件的访问权限, 同时,如果可能,开启必要的ACL进程的UID, GID
严格控制root的使用,尽可能小的在进程间共享UID,GID文件系统的UID, GID
可以使用POSIX提供的saved ID的逻辑来达到一定程度的安全
利用kernel提供的capability及libcap来控制相关的进程权限
使用两个分离的进程,一个进程完成需要高权限的事情,而低权限的那个进程来完成其余事情. 关键是控制好这两个进程之间的通信.
Linux内核可以从文件系统层来干预某个进程的UID,GID的限制.比如setfsuid,setfsuid. 可以使得一个网络daemon可以使用superuser的权限来创建一个特殊的socket, 而它可以被普通用户读写.文件描述的各种操作模式
可以在打开文件描述的时候,指定丰富的权限, 当然也需要认真的控制及设计.资源分配的权限
举了一个很有意思的例子:
如果对于一个文件描述即需要读也许要写权限, 但是读的情况比较多, 很少写,那么就需要创建两个描述符, 一个用来读,并保持始终打开状态, 而另一个在需要的时候才打开.如下:
int reopen_rw(int fd) { char buf[40]; sprintf(buf, "/proc/self/fd/%d", fd); newfd = open(buf, O_RDWR); dup2(newfd, fd); close(newfd); return fd; }
基于经验及分析的基础, 通过setrlimit来设定合适的资源限制
通过Unix Domain Socket来验证1.5 真正的随机数
如果两个进程通过Unix Domain Socket来通信, 那么可以通过getsocketopt()来获得相关进程的UID,GID,从而进行必要的认证
struct ucred caller; socklen_t optlen = sizeof(caller); getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &caller, &optlen);或者使用getuid, getgid之类的函数获得自己的值, 在使用类似sendmsg()之类函数之前填充到对应的msghdr的结构体中.
信号的起始
为了增强对信号处理的检查, 请使用新的siginfo接口来注册信号处理函数.
void (*siginfo_handler) (int signo, siginfo_t *info, void *context);在编写信号处理函数时,需要进行对信号来源进行检查,如下面对Segment fault信号的检查:
void handler(int sig, siginfo_t *infop, void *ctx) { if (infop->si_code != SEGV_MAPERR && infop->si_code != SEGV_ACCERR) return; ... }这里的SEGV_MAPERR和SEGV_ACCERR,参见对siginfo的详细描述: http://pubs.opengroup.org/onlinepubs/009695399/basedefs/signal.h.html
避免共享
尽量不要使用/tmp,而是使用mount机制,通过"bind mount"来在进程自己的home目录下加载一个属于程序自己的tmp文件系统.
int f_mount (void *arg) { const char *home = getent("HOME"); char buf[strlen(home) + sizeof "/my-tmp"]; sprintf(buf, "%s/my-tmp", home); mount(buf, "/tmp", 0, MS_BIND, 0); int fd = creat("/tmp/foo", 0600); ... exit(0); } int main(void) { char st[50000]; pid_t p = clone(f_mount, st + sizeof(st), CLONE_NEWNS, 0); // CLONE_NEWNS,使得clone出的子进程一个独立的文件系统名字空间. exit( p == -1 ? 1 : 0); }
不同的系统提供的生成随机数的接口的实现各不相同,而同一个系统也会提供多种生成随机数的接口. 这导致我们在使用时要认真考虑.1.6 自动防御
比如, posix中提供的rand_r就因为接收的seed参数的取值范围太小,而不推荐使用.
而推荐使用下述的random函数系,关键在于对srandom和initstate的使用, 使其获得更好的seed
long int random(void); void srandom(unsigned int seed); char *initstate(unsigned int seed, char *state, size_t n);另外, rand48函数系也推荐使用,它支持对浮点数的支持, 并且在所有unix的系统上的实现都一样!
但是, 如果必须要使用PRNG, 那么只能选择random.
同时, 只获取少量随机数的情况下, 还可以使用系统提供的特殊的两个char设备: /dev/random, /dev/urandom.
/dev/random的缺点是,在从设备上读取随机数的时候, 会由于没有足够的随机数而造成进程的block!
而/dev/urandom则不会block.
最后, 随着硬件的发展, 有的系统会自带成生真正随机数的硬件, 如果可能, 可以为这些硬件方便的编写获得随机数的特殊代码.
如, VIA就提供了一个特殊指令去获得自带随机数生成硬件所生成的随机数.
另外关于随机数的生成, 也请参考我写的另外一篇文章< 如何更好的生成随机数>
从redhat enterprise Linux 4之后, 程序可以用FORTIFY SOURCE 这样的宏来编译.关于redhat enterprise Linux引入的安全性的相关资料请参考:
就像其它的xxxx_SOURCE的编译参数一样, 这使得编译过程中加入一些安全检查.
它会引入两种检查:
如果函数操作的内存块的大小是已知的,就会对其进行检查
有以下几种情况, 使得编译器可以知道内存块的大小:
当buffer是在stack上自动分配的, 比如调用alloca()获得
当buffer是在函数内部分配的, 比如调用malloca()
标记那些返回值的被忽略的函数
Section 3 编译最佳实践
extern int foo(int) __attribute((__deprecated__));
__asm(".section .gnu.warning.foo\n\t" ".previous"); static const char foo_deprecated[] __attribute((unused, section(".gnu.warning.foo"))) = "Don’t use foo";
extern int foo(int arg) __attribute((__warn_unused_result__));
这样在foo被调用后, 如果它的返回值没有被使用, 编译器就会发出警告
extern char *strcpy(char *dst, const char *src) __attribute((__nonnull__(1, 2)));这里的'1','2'指明了第一和第二参数不能是NULL.
Section 4 调试技巧
asm (".section cleanup_fns, \"a\"");
#define cleanup_fn(name) \
static void name(void); \
static void (*const fn_##name)(void) \
__attribute((unused,
section("cleanup_fns"))) = &name; \
static void name(void)
它这样被使用:
static const char *myname;
cleanup_fn(free_myname)
{
free(myname);
}
void set_myname(const char *s) {
free(myname);
myname = strdup(s);
}
const char *get_myname(void) {
return myname;
}
//这两个symbol(__start, __stop)是由gcc linker根据elf的section自动创建的,
// gcc linker自动创建symbol的规则:
// 1. 对应的section的名字是符合C语言规则的
// 2. section其中的symbol被引用了
extern void (*const __start_cleanup_fns) (void);
extern void (*const __stop_cleanup_fns) (void);
void run_cleanup(void)
{
void (*const *f)(void) = &__start_cleanup_fns;
while (f < &__stop_cleanup_fns)
(*f++)();
}
// 也提供了一个简化版本, 这里clean动作只是简单的调用free,所以,可以不用定义一个特别的函数来做事情
static const char *cleanup_ptr(myname);
void set_myname(const char *s) {
free(myname);
myname = strdup(s);
}
const char *get_myname(void) {
return myname;
}
extern void *const __start_cleanup_ptrs;
extern void *const __stop_cleanup_ptrs;
void run_cleanup(void)
{
void *const *p = &__start_cleanup_ptrs;
while (p < &__stop_cleanup_ptrs)
free(*p++); // 请注意,这里是直接使用free函数,参数是上面定义的函数指针. 而上面的例子, 这里是用定义的函数指针, 而该函数指针内部再调用free.
}
int main(void) { mtrace(); void *p = malloc(10); malloc(20); free(p); return 0; } xxx$export MALLOC_TRACE=mout xxx$mtrace [program_name] mout
int mcheck (void (*afct)(enum mcheck_status)); int mcheck_pedantic (void (*afct)(enum mcheck_status)); void mcheck_check_all(void);
附录A:
#define malloc(sz) \ ({ char __line[strlen(__FILE__) + 6 * sizeof(size_t) + 3]; \ size_t __sz = sz; \ int __n = sprintf(__line, "%c%s:%zu:%zu", \ ’\0’, __FILE__, __LINE__, __sz) + 1; \ size_t __pre = roundup(__n, 2 * sizeof(size_t)); \ char *__p = malloc(__sz + __pre + __n); \ if (__p != NULL) \ { \ memset(mempcpy(__p, __line, __n - 1), ’ ’, __pre - __n); \ __p[__pre - 1] = ’\0’; \ memcpy(__p + __pre + __sz, __line, __n); \ } \ (void *) (__p + __pre); }) #define calloc(szc, n) \ ({ size_t __b = (szc) * (n); \ char *__p = malloc(__b); \ __p != NULL ? memset(__p, ’\0’, __b) : NULL; }) #define realloc(oldp, szr) \ ({ char *__oldp = oldp; \ char *__cp = __oldp; \ while (*--__cp != ’:’); \ size_t __oldszr = atol(__cp + 1); \ size_t __szr = szr; \ char *__p = malloc(__szr); \ if (__p != NULL) \ { \ memcpy(__p, __oldp, MIN(__oldszr, __szr)); \ free(__oldp); \ } \ (void *) __p; }) #define free(p) \ (void) ({ char *__p = (char *) (p) - 1; \ while (*--__p != ’\0’); \ free(__p); }) #define valloc(szv) \ ({ void *__p; \ posix_memalign(&__p, getpagesize(), szv) ? NULL : __p; }) #define pvalloc(szp) \ ({ void *__p; \ size_t __ps = getpagesize(); \ posix_memalign(&__p, __ps, roundup(szp, __ps)) ? NULL : __p; }) #define cfree(p) \ free(p) #define posix_memalign(pp, al, szm) \ ({ size_t __szm = szm; \ size_t __al = al; \ char __line[strlen(__FILE__) + 6 * sizeof(size_t) + 3]; \ int __n = sprintf(__line, "%c%s:%zu:%zu", \ ’\0’, __FILE__, __LINE__, __szm) + 1; \ size_t __pre = roundup(__n, __al); \ void *__p; \ int __r = posix_memalign(&__p, __al, __pre + __szm + __n); \ if (__r == 0) \ { \ memset(mempcpy(__p, __line, __n - 1), ’ ’, __pre - __n); \ __p[__pre - 1] = ’\0’; \ memcpy((char *) __p + __pre + __szm, __line, __n); \ *pp = (void *) ((char *) __p + __pre); \ } \ __r; }) #define memalign(al, sza) \ ({ void *__p; \ posix_memalign(&__p, al, sza) == 0 ? __p : NULL; }) #define strdup(s) \ ({ const char *__s = s; \ size_t __len = strlen(__s) + 1; \ void *__p = malloc(len); \ __p == NULL ? NULL : memcpy(__p, __s, __len); })
相关资料:
http://www.lst.de/~okir/blackhats/ 其中分析了很多文中提到的缺陷原理,比如setuid, sprintf, ...