Web3安全-跨链项目/协议相关漏洞分析-1
区块链安全
通过链上真实黑客攻击分析一些桥相关协议或者涉及到桥实现漏洞被黑的。 最后也找了两个类似的漏洞报告中出现的分析一下。
通过链上真实黑客攻击分析一些桥相关协议或者涉及到桥实现漏洞被黑的。 最后也找了两个类似的漏洞报告中出现的。 我可以想到的最近的有:port3,GyroStable,CrossCurve,Sage,Spoke 其中GriffinAI,Seedify SFUND Bridge,Yala,Shibarium Bridge设计到私钥/内部权限等问题就不说了。 0xswapnet ApertureFinance关于任意调用导致transferFrom()这类就不说了,因为并没有涉及到桥协议。但是后续在看审计报告也发现了类似的。 后面有可以在补充。 ### 跨链基本了解 跨链基本原理其实就是“锁定 + 铸造” / “烧毁 + 解锁”。 其实只知道参数就可以了。参数名称和实现都大差不差。 注意验证方式也都不太一样,但是也就那几种:多节点 MPC,多签,去中心化预言机+DVN,多签 + 外部验证者.... #### 一些常见参数: | 参数名称(常见叫法) | 类型 | 含义 / 例子(BSC→ETH) | 是否必填 | 备注 | |---|---|---|---|---| | token | address | BSC上的代币地址 | 必填 | | | amount | uint256 | 数量 | 必填 | — | | toChainId | uint16/uint256 | 目标链ID | 必填 | 每个桥的链ID定义可能略不同 | | toAddress/recipient | bytes32/address | 收币地址 | 必填 | 有时要转成 bytes32 | | fromChainId /sourceChain | uint16/uint256 | 来源链ID | — | 验证用 | | nonce / sequence | uint64/uint256 | 防重放序号 | — | 防止同笔消息重复执行 | | payload / vaa / message | bytes | 编码后的全部跨链信息(上面字段打包) | — | Wormhole叫VAA,LayerZero叫payload | | signatures | bytes\[\] | 验证者签名数组 | — | 多签桥要收集足够数量签名 | | transferFee / protocolFee | uint256 | 手续费 | -- | — | 真实世界: ----- ### port3 黑客地址:[0xb13a503da5f368e48577c87b5d5aec73d08f812e](https://bscscan.com/txs?a=0xb13a503da5f368e48577c87b5d5aec73d08f812e&ps=100) 漏洞合约: <https://bscscan.com/address/0xb4357054c3da8d46ed642383f03139ac7f090343#code> 直接看最黑客最开始的操作。 [https://app.blocksec.com/phalcon/explorer/tx/bsc/0xfaf450571541b95f924024ac3febd5cf6c16695ce787217ca8870350309051c1?debugLine=0&line=0&showSStore=true&showStaticCall=true](https://app.blocksec.com/phalcon/explorer/tx/bsc/0xfaf450571541b95f924024ac3febd5cf6c16695ce787217ca8870350309051c1?debugLine=0&line=0&showSStore=true&showStaticCall=true) 首先进入了`registerChain`(哪些链上的哪个代币合约地址发来的跨链消息是可信的,可以让我 mint 出代币。)  但是这里有个问题明明有修饰符: ```js /// @dev verify owner is caller or the caller has valid owner signature modifier onlyOwnerOrOwnerSignature( CATERC20Structs.SignatureVerification memory signatureArguments ) { if (_msgSender() == owner()) { _; } else { bytes32 encodedHashData = prefixed( keccak256( abi.encodePacked(signatureArguments.custodian, signatureArguments.validTill) ) ); require(signatureArguments.custodian == _msgSender(), "custodian can call only"); require(signatureArguments.validTill > block.timestamp, "signed transaction expired"); require( isSignatureUsed(signatureArguments.signature) == false, "cannot re-use signatures" ); setSignatureUsed(signatureArguments.signature); require( verifySignature(encodedHashData, signatureArguments.signature, owner()), "unauthorized signature" ); _; } ``` 读了一下\_OWNER是0地址,0x0000000000000000000000000000000000000000。 所以owner == address(0)。 在`verifySignature`也可以看到authority是0地址(OWNER)【没有做检查】 最终来到 ```js function verifySignature( bytes32 message, bytes memory signature, address authority ) internal pure returns (bool) { (uint8 v, bytes32 r, bytes32 s) = splitSignature(signature); address recovered = ecrecover(message, v, r, s); if (recovered == authority) { return true; } else { return false; } } ``` 现在任何签名让 ecrecover 恢复出 address(0),就会被认为是“owner 签名 所以现在黑客地址变成了合法的远程代币合约地址” 现在就可以开始mint了。发出 Wormhole 消息伪造 VM(因为 emitterAddress 被注册了,所以合约认为“合法”) <https://app.blocksec.com/phalcon/explorer/tx/bsc/0x34c17a91b2f2ccd5973ecd49c20cc3c0939c5d8eaeeb740e9dec97fb1345e1da> ```js function bridgeIn(bytes memory encodedVm) external returns (bytes memory) { require(isInitialized() == true, "Not Initialized"); require(evmChainId() == block.chainid, "unsupported fork"); (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole().parseAndVerifyVM( encodedVm ); require(valid, reason); require( //利用漏洞注册成了合法的 emitterAddress bytesToAddress(vm.emitterAddress) == address(this) || tokenContracts(vm.emitterChainId) == vm.emitterAddress, "Invalid Emitter" ); ``` 执行 bridgeIn → mint 了 10 亿枚 PORT3 #### 根本原因: 上述讲过了,"在`verifySignature`也可以看到authority是0地址(OWNER)【没有做检查】" ### GyroStable 黑客地址: 0x7DD4075A6eAe9f18309F112364f0394C2DfA8102 漏洞合约: arb:0xCA5d8F8a8d49439357d3CF46Ca2e720702F132b8(漏洞利用时候的impl: 0xE8ab4550dFa163753023Da3154234a525C8eF863) eth:0xa1886c8d748deb3774225593a70c79454b1da8a6(impl:0xe07f9d810a48ab5c3c914ba3ca53af14e4491e8a) 我认为逻辑在arb侧的L2Gyd.bridgeToken()的和eth侧的GydL1CCIPEscrow.ccipReceive() 都有问题。但是eth侧的GydL1CCIPEscrow.ccipReceive()问题最大,一般接收消息更应该做好处理。 3次tx完成整个攻击。 TX1: <https://app.blocksec.com/phalcon/explorer/tx/eth/0x45739a92c2d99f172a74d8028736a2fd1b507ac6fc134680cd1dccd3c572c600> 第一个Tx是利用漏洞的,当时第一眼就看到了0.000000000000000001GYD。 (在绝大多数协议中,除了价格问题,发送1WEI或者极小值那就是为了发送其他data来使用的。这里因为没有涉及到其他的借款借贷等操作,所以是为了实现一些data)。 [https://calldata.swiss-knife.xyz/decoder?tx=0x51c22898a9b9f519a10b0a0be89b9d51c0248adb80cc0f89e57437e15e6c60c7&chainId=42161](https://calldata.swiss-knife.xyz/decoder?tx=0x51c22898a9b9f519a10b0a0be89b9d51c0248adb80cc0f89e57437e15e6c60c7&chainId=42161) 可以看到调用bridgeToken()方法传入了approve给spender地址超大额的授权。  这会发送CCIP消息到L1 escrow([https://etherscan.io/address/0x79ec4a4440878b25b0cbd902fb3414015c28bbc5)。](https://etherscan.io/address/0x79ec4a4440878b25b0cbd902fb3414015c28bbc5)%E3%80%82) TX2: [https://app.blocksec.com/phalcon/explorer/tx/eth/0x45739a92c2d99f172a74d8028736a2fd1b507ac6fc134680cd1dccd3c572c600?line=36&showStaticCall=true](https://app.blocksec.com/phalcon/explorer/tx/eth/0x45739a92c2d99f172a74d8028736a2fd1b507ac6fc134680cd1dccd3c572c600?line=36&showStaticCall=true) ETH侧的GydL1CCIPEscrow.ccipReceive()验证发送者(Arbitrum的L2Gyd),解码消息,转移1 wei到GYD(无影响,只是满足参数),然后执行GYD.functionCall(data) → GYD.approve(attacker, max),以escrow为msg.sender。  攻击完成闭环。 TX3: <https://app.blocksec.com/phalcon/explorer/tx/eth/0xe03ac744df1910a71fedab58bc6a32ab5afe1cb4fcad94a0e5c8d7edf0d7405c?showStaticCall=true> 最后 攻击者直接调用GYD的transferFrom(escrow地址, attacker地址, escrow的GYD余额),抽走全部资金。 #### 根本原因: 在ARB侧L2Gyd.bridgeToken()函数中。 这里允许用户任意指定 recipient 和 data,没有任何限制。  这里的ccipReceive()也有问题,不说了。和ETH侧一样。 ETH侧的GydL1CCIPEscrow.ccipReceive() ```js /// @dev handle a received message /// the authentification is done in the parent contract function _ccipReceive(Client.Any2EVMMessage memory any2EvmMessage) internal override { address expectedSender = chainsMetadata[any2EvmMessage.sourceChainSelector].targetAddress; if (expectedSender == address(0)) { revert ChainNotSupported(any2EvmMessage.sourceChainSelector); } address actualSender = abi.decode(any2EvmMessage.sender, (address)); if (expectedSender != actualSender) { revert MessageInvalid(); } //没验证 (address recipient, uint256 amount, bytes memory data) = abi.decode(any2EvmMessage.data, (address, uint256, bytes)); uint256 bridged = totalBridgedGYD; bridged -= amount; totalBridgedGYD = bridged; gyd.safeTransfer(recipient, amount); if (data.length > 0) { recipient.functionCall(data); } emit GYDClaimed( any2EvmMessage.sourceChainSelector, recipient, amount, bridged ); } ``` 这里允许任意 recipient + 任意 data → recipient.functionCall(data) ### CrossCurve 这起攻击也有其他机器人参与,这里只举例一个 黑客地址: 0x632400f42e96a5deb547a179ca46b02c22cd25cd 漏洞合约:0xb2185950f5a0a46687ac331916508aada202e063 攻击TX: <https://app.blocksec.com/phalcon/explorer/tx/eth/0x37d9b911ef710be851a2e08e1cfc61c2544db0f208faeade29ee98cc7506ccc2> 其实刚开始看的时候我还不知道expressExecute和Execute有什么区别,后面才知道这个是漏洞利用的关键。所以看了攻击tx发现有点离谱(攻击者直接就),进入后几个验证就到了PortalV2.unlock解锁和释放资产了。 首先看传输路由:交易先进入expressExecute。  随后经过一个简单的验证 if (gateway.isCommandExecuted(commandId)) revert AlreadyExecuted(); 来判断commandId是不是之前有过的。 然后继续往下走。 进入了AxelarExpressExecutable.setExpressExecutor setExpressExecutor(记录调用者为 expressExecutor),并调用 execute(sourceChain, sourceAddress, payload) 来执行 payload。 随后进入了AxelarExpressExecutable.execute 看execute的实现: ```js function execute( bytes32 commandId, string calldata sourceChain, string calldata sourceAddress, bytes calldata payload ) external { bytes32 payloadHash = keccak256(payload); if (!gateway.validateContractCall(commandId, sourceChain, sourceAddress, payloadHash)) revert NotApprovedByGateway(); address expressExecutor = _popExpressExecutor(commandId, sourceChain, sourceAddress, payloadHash); if (expressExecutor != address(0)) { // slither-disable-next-line reentrancy-events emit ExpressExecutionFulfilled(commandId, sourceChain, sourceAddress, payloadHash, expressExecutor); } else { _execute(sourceChain, sourceAddress, payload); //随后进入 } } ``` 如果直接向execute传参,那么会进入 gateway.validateContractCall(commandId, sourceChain, sourceAddress, payloadHash) 。 会验证消息的来源(sourceChain、sourceAddress)和 payload 的哈希是否由 Axelar 网关认证。 但是向expressExecute传参会进入\_execute(sourceChain, sourceAddress, payload); 由于是ReceiverAxelar,所以直接绕过了检查。 然后进入了Receiver.receiveData传入黑客构造的恶意参数。  然后就是CoreFacet.resume 然后到 PortalV2.unlock。这里就不详细说了,漏洞不在这里。  综上:expressExecute 本意是 Axelar 的“快速执行”模式,允许用户预支付 gas 来加速跨链调用(后续由真实消息退款)。但在 CrossCurve 的实现中,它允许攻击者注入任意 sourceChain、sourceAddress 和 payload,直接触发内部逻辑,而不等待真实跨链消息的到来和验证。这相当于打开了一个“后门”,让攻击者伪造跨链指令。 #### 根本原因: expressExecute 是函数完全公开、无权限控制、无来源验证的入口 该函数允许任何人用任意参数直接调用 \_execute,而没有任何跨链消息真实性校验。 ```js function expressExecute( bytes32 commandId, string calldata sourceChain, string calldata sourceAddress, bytes calldata payload ) external payable virtual { if (gateway.isCommandExecuted(commandId)) revert AlreadyExecuted(); // 下面没有任何验证就直接信任了调用者提供的所有参数 address expressExecutor = msg.sender; bytes32 payloadHash = keccak256(payload); emit ExpressExecuted(commandId, sourceChain, sourceAddress, payloadHash, expressExecutor); _setExpressExecutor(commandId, sourceChain, sourceAddress, payloadHash, expressExecutor); _execute(sourceChain, sourceAddress, payload); } ``` ### Sage 这个后面单独发,因为得审一下基础设施。 ### Spoke 这次攻击其实不涉及到跨桥协议。 之前分析过,直接贴过来了。 黑客地址: <https://bscscan.com/address/0xc6e8210e47602860c97edc0bd7556641f048868e> 漏洞合约: <https://bscscan.com/address/0xdf4fFDa22270c12d0b5b3788F1669D709476111E> 攻击合约: <https://bscscan.com/address/0xe4b8d09c12d15f2934f21ca8eaa5dcb1464f9ed1> 攻击TX: <https://app.blocksec.com/phalcon/explorer/tx/bsc/0xcd345310e491195f0500d45d6987eaef342bae24390f4da4f7e6749b8105b4c3> 这个漏洞调用栈比较少, 跟着调用流程Debug:  可以看到先进行sponsorOrderUsingPermit2()函数操作。 sponsorOrderUsingPermit2()接收3个参数()signature不说了。 看传参: ```json order:[ { fromAddress:"0xe4b8d09c12d15f2934f21ca8eaa5dcb1464f9ed1" toAddress:"0xe4b8d09c12d15f2934f21ca8eaa5dcb1464f9ed1" filler:"0xe4b8d09c12d15f2934f21ca8eaa5dcb1464f9ed1" fromToken:"0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d" //USDC toToken:"0x0000000000000000000000000000000000000000" expiry:"1,759,825,777" fromAmount:"15,121,014,087,537,935,893,820" fillAmount:"0" feeRate:"0" fromChain:"56" toChain:"56" postHookHash:"0x0000000000000000000000000000000000000000000000000000000000000000" } ``` ```json permit:[ {permitted:[ {token:"0xe4b8d09c12d15f2934f21ca8eaa5dcb1464f9ed1" amount:"15,121,014,087,537,935,893,820"}] nonce:"59,946,715,367,315,085,452,573,067,176,193,223,667,943,792,355,376,429,741,185,393,233,268,031,513,012" deadline:"1,759,825,777"} ``` 根据参数可以拿出来主要信息: order.fromToken:"0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d" //USDC permit.permitted.token:"0xe4b8d09c12d15f2934f21ca8eaa5dcb1464f9ed1" //攻击者合约,里面有erc20实现,所以被认为token。 看一下sponsorOrderUsingPermit2方法的实现: ```js /// @inheritdoc ISpoke function sponsorOrderUsingPermit2( Order calldata order, IPermit2.PermitTransferFrom calldata permit, bytes calldata signature ) external nonReentrant { bytes32 orderHash = keccak256(abi.encode(order)); if (orderHashToStatus[orderHash] != OrderStatus.EMPTY) revert OrderAlreadyExists(); if (block.timestamp > order.expiry) revert OrderExpired(); if (order.fromChain != _getChainId()) revert InvalidSourceChain(); if (order.fromToken == Utils.NATIVE_TOKEN) revert NativeTokensNotAllowed(); orderHashToStatus[orderHash] = OrderStatus.CREATED; IPermit2.SignatureTransferDetails memory transferDetails; transferDetails.to = address(this); transferDetails.requestedAmount = order.fromAmount; bytes32 witness = _hashOrderTyped(order); permit2.permitWitnessTransferFrom( permit, transferDetails, order.fromAddress, witness, Utils.ORDER_WITNESS_TYPE_STRING, signature ); emit OrderCreated(orderHash, order); } ``` 可以看出来该函数是一个用于创建订单的接口。 通过 Permit2(这里不详细解释了) 机制,从 order.fromAddress 转移 permit.permitted.token 的 order.fromAmount 到合约,创建订单并标记状态为 CREATED。 还可以跨链或同链代币交易场景。 ```js struct Order { // Address that will supply the fromAmount of fromToken on the fromChain. address fromAddress; // Address to receive the fillAmount of toToken on the toChain. address toAddress; // Address that will fill the Order on the toChain. address filler; // Address of the ERC20 token being supplied on the fromChain. // 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE in case of native token. address fromToken; // Address of the ERC20 token being supplied on the toChain. // 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE in case of native token. address toToken; // Expiration in UNIX for the Order to be created on the fromChain. uint256 expiry; // Amount of fromToken to be provided by the fromAddress. uint256 fromAmount; // Amount of toToken to be provided by the filler. uint256 fillAmount; // Protocol fees are taken out of the fromAmount and are calculated within the Spoke.sol // contract for single chain orders or on the Hub for cross chain orders. // The following formula determines the amount of fromToken reserved as fees: // fee = (fromAmount * feeRate) / 1000000 uint256 feeRate; // Chain ID of the chain the Order will be created on. uint256 fromChain; // Chain ID of the chain the Order will be filled on. uint256 toChain; // Keccak256 hash of the abi.encoded ISquidMulticall.Call[] calldata calls that should be provided // at the time of filling the order. bytes32 postHookHash; } ``` 根据攻击者输入结合合约代码可以看出:合约未验证 permit.permitted.token == order.fromToken 那么**攻击流程**就很明了了: 攻击者存入假代币 记录USDC 后续可以通过 refundOrder() 提取USDC 现在类比下整个流程: - 你告诉银行(合约)要存 1000 USDC(order.fromToken) - 但你通过签名(permit)实际给了 1000 张废纸(假代币) - 银行记录你存了 1000 USDC(orderHashToStatus = CREATED) - 后来你说“退款refundOrder ”,银行按记录退给你 1000 USDC,而没检查你实际存了什么钱 #### 根本原因 合约中sponsorOrderUsingPermit2函数中没有正确验证 `permit.permitted.token` 与 `order.fromToken` 一致,攻击者可以伪造一个签名, 把假代币地址传进去,导致“用假币换真币” 一些漏洞报告里面的 --------- ### LI.FI:Too generic calls in GenericBridgeFacet allow stealing of tokens 漏洞报告: <https://solodit.cyfrin.io/issues/too-generic-calls-in-genericbridgefacet-allow-stealing-of-tokens-spearbit-lifi-pdf> 这个漏洞是22年发现的,会发现最近的几起攻击,比如0xswapnet ApertureFinance有点类似。(只是攻击路径有点不同,但是原理差不多。因为没分析0xswapnet ApertureFinance,所以这里来说一下) 相关漏洞代码:GenericBridgeFacet: <https://github.com/lifinance/contracts/blob/f024ee5d64a24882010642bf81d87529712edc7c/src/Facets/GenericBridgeFacet.sol#L69-L120> LibSwap: <https://github.com/lifinance/contracts/blob/f024ee5d64a24882010642bf81d87529712edc7c/src/Libraries/LibSwap.sol#L30-L68> 在LibSwap.swap()中 ```js // solhint-disable-next-line avoid-low-level-calls (bool success, bytes memory res) = _swapData.callTo.call{ value: nativeValue }(_swapData.callData); ``` 这里,`_swapData.callTo` 和 `_swapData.callData` 完全由用户输入控制。 没有检查callTo是否是合法的DEX地址,也没有验证callData是否是安全的函数签名 在GenericBridgeFacet.startBridge()中 ```js (bool success, bytes memory res) = _bridgeData.callTo.call{ value: value }(_bridgeData.callData); ``` 同样,`_bridgeData.callTo` 和 `_bridgeData.callData` 来自用户输入。 这种设计是为了支持“任意桥接”,但它本质上是一个“任意代码执行”入口点。Solidity的低级调用允许执行任何合约的任何函数,只要有足够的权限。 在LibSwap.swap()中,在调用前执行: ```js if (!LibAsset.isNativeAsset(fromAssetId)) { LibAsset.maxApproveERC20(IERC20(fromAssetId), _swapData.approveTo, fromAmount); //调用这个 if (toDeposit != 0) { LibAsset.transferFromERC20(fromAssetId, msg.sender, address(this), toDeposit); } } else { nativeValue = fromAmount; } ``` 这会批准approveTo(用户指定)从合约中转移fromAmount数量的代币。 在\_startBridge()中 ```js LibAsset.maxApproveERC20(IERC20(_bridgeData.assetId), _bridgeData.callTo, _bridgeData.amount); ``` 批准callTo从合约转移代币。 问题在于:如果攻击者将callTo设置为受害者代币的合约地址,并将callData编码为`transferFrom(victim, attacker, amount)`,那么合约就会代表自己(LiFi合约)调用transferFrom()。如果受害者之前批准了LiFi合约转移其代币,攻击者就能窃取这些代币。 在swapAndStartBridgeTokensGeneric()中,不直接从msg.sender转移;它依赖交换逻辑。 但在LibSwap.swap()中,如果需要,它会从msg.sender转移额外代币到合约: ```js if (toDeposit != 0) { LibAsset.transferFromERC20(fromAssetId, msg.sender, address(this), toDeposit); } ``` 这确保合约有足够的余额进行交换,但结合任意调用,攻击者可以先转移自己的小额代币,然后用任意调用窃取他人代币。 综上,攻击就很清楚了: 攻击者调用swapAndStartBridgeTokensGeneric(): 构造一个LibSwap.SwapData 或 BridgeData: ```php { callTo:设置为受害者代币的地址 approveTo:设置为USDC合约地址(或攻击者控制的地址)。 callData:编码为USDC的transferFrom(victim, attacker, amount) 函数调用:transferFrom(address,address,uint256)", victim, attacker, amount sendingAssetId 和 receivingAssetId 可以设置为任意,只要匹配代币就可以。 } ``` #### 根本原因 合约允许用户完全控制低级调用的目标地址(callTo)和调用数据(callData),却没有对调用目标和函数签名进行任何有效限制或白名单校验,导致攻击者可以直接让合约调用任意 ERC20 的 transferFrom(),窃取已授权给 LiFi 合约的大额代币。 ### LI.FI: Bridge with Axelar can be stolen with malicious external call 这个也不说了,有点像CrossCurve的。 但是有一些区别: 这个漏洞的Receiver 在"已验证的跨链消息"上执行了危险的任意逻辑. 相当于合法 Axelar 消息 + 过度信任 payload 这个就不分析了: 报告链接: <https://solodit.cyfrin.io/issues/bridge-with-axelar-can-be-stolen-with-malicious-external-call-spearbit-lifi-pdf> 涉及到的代码: <https://github.com/lifinance/contracts/blob/f024ee5d64a24882010642bf81d87529712edc7c/src/Periphery/Executor.sol#L272-L288> <https://github.com/lifinance/contracts/blob/f024ee5d64a24882010642bf81d87529712edc7c/src/Periphery/Executor.sol#L323-L333> <https://github.com/lifinance/contracts/blob/f024ee5d64a24882010642bf81d87529712edc7c/src/Periphery/Executor.sol#L269-L288> ### 文章公众号: [https://mp.weixin.qq.com/s/\_flzw2xzY81HdXvLbI6lwA](https://mp.weixin.qq.com/s/_flzw2xzY81HdXvLbI6lwA)
发表于 2026-04-01 09:00:06
阅读 ( 360 )
分类:
区块链安全
0 推荐
收藏
0 条评论
S7iter
1 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!