[置顶] 拜读大牛Ulrich Drepper大作之Defensive Programming for Red Hat Enterprise Linux

读大牛Ulrich Drepper 关于写安全的代码的心得及记录。


关键点

Section 2 Safe Programming

  1. C/C++的安全问题主要爆发在memory的管理上, 本节主要讲解如何避免这些经常被提及的内存问题
    1.1 关于处理C语言中对memory管理的问题
    memory的边界
        提供了一个宏,来更好的防止调用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.2 预防基于文件系统的攻击

    综述了类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 给了以下的一个列表, 请大家使用的时候多留意.
        [置顶] 拜读大牛Ulrich Drepper大作之Defensive Programming for Red Hat Enterprise Linux_第1张图片
        这里需要注意下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.3 降低风险
    尽量限制一个程序所要的特权.
    一个程序可以要求以下的特权:
        内存访问保护, 特别是在IA-32上的Exec-Shield.
    注意由mmap, mprotect所管理的内存的权限, 尽量限制对内存操作的权限. 这需要在设计时就有所考量.
        文件访问保护
    同样,尽可能限制对一个文件的访问权限, 同时,如果可能,开启必要的ACL
        进程的UID, GID
    严格控制root的使用,尽可能小的在进程间共享UID,GID
    可以使用POSIX提供的saved ID的逻辑来达到一定程度的安全
    利用kernel提供的capability及libcap来控制相关的进程权限
    使用两个分离的进程,一个进程完成需要高权限的事情,而低权限的那个进程来完成其余事情. 关键是控制好这两个进程之间的通信.
        文件系统的UID, GID
    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来设定合适的资源限制
    1.4 不要轻易相信任何人
    通过Unix Domain Socket来验证
        如果两个进程通过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.5 真正的随机数
    不同的系统提供的生成随机数的接口的实现各不相同,而同一个系统也会提供多种生成随机数的接口. 这导致我们在使用时要认真考虑.
    比如, 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就提供了一个特殊指令去获得自带随机数生成硬件所生成的随机数.

    另外关于随机数的生成, 也请参考我写的另外一篇文章< 如何更好的生成随机数>

    1.6 自动防御
    从redhat enterprise Linux 4之后, 程序可以用FORTIFY SOURCE 这样的宏来编译.
    就像其它的xxxx_SOURCE的编译参数一样, 这使得编译过程中加入一些安全检查.
    它会引入两种检查:
        如果函数操作的内存块的大小是已知的,就会对其进行检查
            有以下几种情况, 使得编译器可以知道内存块的大小:
                当buffer是在stack上自动分配的, 比如调用alloca()获得
                当buffer是在函数内部分配的, 比如调用malloca()
        标记那些返回值的被忽略的函数
                关于redhat enterprise Linux引入的安全性的相关资料请参考:
                http://www.redhat.com/magazine/009jul05/features/execshield/
                http://www.redhat.com/f/pdf/gov/DHSBattlecard.pdf


Section 3 编译最佳实践

  1. 关注warnings
    打开-Wall, -Wextra编译选项, 对于后者容易造成较多误报, 需看情况启用.
    对于C++, 推荐再打开一个-Weffc++的选项
    某些如由flex, bison产生的代码很难去掉所有的警告

  2. 避免库函数中的一些不好的接口
    对于设计接口的人, 如果需要废弃一些接口, 请给于它以下的声明:
    extern int foo(int)
    __attribute((__deprecated__));
    

    对于需要在linker时显示的错误, 请使用如下方式: 它只会在用它来制作DSO的时候才会出现, 并且除此之外, 不会出现在其它比如execute的文件中.
    __asm(".section .gnu.warning.foo\n\t"
    ".previous");
    static const char foo_deprecated[]
    __attribute((unused,
    section(".gnu.warning.foo")))
    = "Don’t use foo";
    

  3. 注意未使用的返回值
    尽可能的对函数的返回值做检查, 尤其是调用一些库函数的时候. 如, setuid等
    对于库的实现者, 最好对暴露给用户的接口进行以下分装:
    extern int foo(int arg)
    __attribute((__warn_unused_result__));
    

    这样在foo被调用后, 如果它的返回值没有被使用, 编译器就会发出警告

  4. 注意作为参数的空指针
    GNU提供了一种方法来检测传递给函数的NULL指针.
    extern char *strcpy(char *dst,
    const char *src)
    __attribute((__nonnull__(1, 2)));
    
    这里的'1','2'指明了第一和第二参数不能是NULL.
    它只能检测到编译时的NULL指针!!


Section 4 调试技巧

  1. 内存处理相关的Debug
    运行时测试
        依赖于glibc的malloc系函数的出错信息来帮助调试.
        同时, mudflap及valgrind都是不错的检查工具.
        本文的附录A,提供了一个实用的对malloc系函数的macro分装, 能很好的增加有用的调试信息. 这让我想起来很久之前看到的一些fujitsu的代码,好像就用了这样的技术.

    释放所有的东西
        这里介绍了一个很有用的利用elf的section及gcc的linker来进行释放动作的定制!!!值得仔细看看哦!!!
        它不会给整个程序带来性能损失.
        如下:
            定义个一个指向释放动作的函数指针
            把它放入elf中的特殊的section
            当我们需要释放动作时, 就遍历所有上述section的函数指针, 去做对应的释放动作
           
    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.
    }
    
    
    

    未初始化的内存
    这里阐述了一个性能方面的问题:
        不要显示的初始化任何你分配得到的内存, 因为这样有可能浪费效率. 只有在不可避免要初始化时才去初始化.
        在使用动态分配内存,并要求分配到的内存都初始化为0的时候,请使用calloc(它会更好的帮我们做关于分配到的内存是否需要被clean的判断)代替malloc+memset的组合
        在调试阶段, 建议使用MALLOC_PERTURB_宏或者mallopt来帮助检查内存初始话的问题, 当然valgrind也可以做到. 正式发行代码,就不要使用了,会带来比较大的性能影响

    hook内存分配
    GNU Libc提供了mtrace的机制帮助提供内存分配的hook信息
    请注意: 它不能用于多线程的程序!!!
        mtrace()在函数main起始处调用
        打开环境变量中的MALLOC_TRACE宏
        mtrace script: 用来分析mtrace hook之后产生的所有有关内存动作的信息
    int main(void) {
        mtrace();
        void *p = malloc(10);
        malloc(20);
        free(p);
        return 0;
    }
    
    xxx$export MALLOC_TRACE=mout
    
    xxx$mtrace [program_name] mout
    

    GNU Libc还提供了另外一个mcheck的接口, 但是同样 ,它也不能用于多线程!!!
    int mcheck
    (void (*afct)(enum mcheck_status));
    int mcheck_pedantic
    (void (*afct)(enum mcheck_status));
    void mcheck_check_all(void);

    有关Memory处理调试的产品
    这节里面说了各个工具的设计理念及一些限制,值得在使用的时候参考!!!
    介绍了Redhat Enterprise Linux中自带的一些产品
    dmalloc
        重新实现了malloc的新的lib, 增加了新的运行时检测
    ElectricFence
        与dmalloc不同的另外一套实现, 把所有的内存动作都由mmap来处理, 这样可以增加更多的检测
    valgrind,
       不用修改原始binary, 它是跑在自己的vm中来帮助收集信息, 有一系列的限制. 对arm的支持有限, 有自己的多线程库,使得用标准libthread开发的程序时,有可能会有问题
    mudflap
       需要加上-fmudflap -lmudflap等选项重新编译程序

  2. 使用Debug模式
    gnu的libstdc++库提供了不少特别的debug模式编译选项, 通过它们可以更好的帮助我们发现问题.但是,同样的不要用在产品的release版本.
    比如, GLIBCXX_DEBUG的编译宏, 将在程序出错的时候提供更多的详细信息

  3. 自动化的backtraces
    为了提供更多程序崩溃信息,最简单的方法就是把程序与libSegFault库链接起来编译.
    它帮助我们在程序启动时,注册很多关于segment fault相关的信号处理程序: SIGSEGV, SIGILL, SIGBUS, SIGSTKFLT, SIGABRT,  SIGFPE
    当这些收到这些信号时,会打出很多额外的调试信息

    同时, 如果没有使用libSegFault连编程序, 那么使用catchsegv脚本,它会在运行时把libSegFault载入. 它有一个很友好的行为: 会帮你自动把一些内存信息decode为对应的函数名,行号之类.

    简单介绍了一下libSegFault所依赖的<execinfo.h>中定义的函数及机制

附录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, ...




你可能感兴趣的:(socket,redhat,File,null,deprecated,linker)