钱包安全:[0day]关于某些使用indexedDB的钱包可能存在私钥泄露问题 前言: 暂时忙完,得到短暂的自由时间,刷推看到,关于钱包安全问题
在这里简单记录下。
使用 indexedDB 来存储加密密钥数据,且如果浏览器存在类型混淆漏洞,可以很任意被进行利用。或者在后利用:当黑客拿下服务器或者个人电脑中马等等的时候,很容易就可以通过无钱包密码拿到私钥
消息来源: @EXVULSEC/ X (twitter.com)
复现: 攻击者用的是chrome浏览器,受害者用的是Brave浏览器。
sui wallet: (不仅是SUI,这里只用SUI来举例)
首先钱包地址:
A(攻击者):
sui钱包密码test666666.
A记住词:agent best wife champion speed sniff about mechanic total stock source crucial
A钱包地址:0x2f50f55ebaa30e0e7b366fdd3480b039104ee03cd1dcf6dbc702ada575414e7f
通过IndeexedDBEdit可以编辑
1 2 3 4 5 6 7 8 9 10 11 12 [ { "key": "f276007a-0083-4130-b37a-e866ff404372", "value": { "id": "f276007a-0083-4130-b37a-e866ff404372", "type": "mnemonic", "encryptedData": "{\"data\":\"/9BE9M2LHza8Vofd2iC2O+4ySsg+H7ujPot4RhjF3RTKR3D/rfLUwFAj+kXGq1qw1gP1zLoEFMKr0ANsTyM89IzeVcP0UMYl3qXZ5eHcNclfkaIc8TH7rC7O0VAcSY6dLiHQAIg7GL2mQFjpjZZSxnfqIa+YVkCU989G7JEmyWyJLZuAWXSdhtgVUZWpkJiECuZ43QDBnwSfVi14k2tFnL9YH2FjQ4S1wEhIm33lV9JK0w/fwknVQTXA1DzHUx64uQgtLguA20wzFor3SeCRxIr4HfY27Q==\",\"iv\":\"Kv0KYLadL5RaOmzYeuRQOA==\",\"salt\":\"9x+odK8Pq0o9Q0eNyT8A6/pRrva1DwRtWyNOGm4OSy8=\"}", "sourceHash": "37539da3c74a9d00e32e508450f3cad728ddde84da3e07a16aed0ce66287a8ac", "createdAt": 1703173323710 } } ]
B(受害者):
sui钱包密码test777777.
B记住词:dream mother rather brass impulse farm gap gasp brush outdoor cruise ripple
B钱包地址:0x0a27cfb016b5e718e580af131b7d8ef0b1e6acd77f6b9c144c51f7447c505f9b
通过IndeexedDBEdit可以编辑
1 2 3 4 5 6 7 8 9 10 11 12 [ { "key": "a804f297-6da8-4454-af7b-6e1e0487329d", "value": { "id": "a804f297-6da8-4454-af7b-6e1e0487329d", "type": "mnemonic", "encryptedData": "{\"data\":\"Newch36wdd3V05fnNq6L5h+uWdr1V7JFcrSpIqfIVwXK6PgX9mGM+CBsYYqBKWr24S0t/b9kgPfpTjLHHvLoJC26HO8T/ffcl6WCBwpvXwkWlQi6LuAHwppfcuBKNM5GS1kwn/v1jje7O63YTx/KdGXnztNDu7gO7QhQkkR3JKqKMjngZVEKDzALYvOVZMV8J86dQzyXJqOBDdu3wCxCG8tBQEYEgCRCP/+OzbDOVuJ0HrHd3ngsF/9X2vKI7VD3uUGdJTdNmacASsQs9+W47ANK4BDQWA==\",\"iv\":\"BgbWWuGuF4q4vOdd0txuIQ==\",\"salt\":\"fSoWM/RmcHWYF8Ty5/P2q1Jm03s83sVulTxadaX7UUQ=\"}", "sourceHash": "263fe89596a8b156c0626f2eb9a066bab1ee9228bcdefe923728990c88252864", "createdAt": 1703176709184 } } ]
我们将B的encryptedData和sourceHash替换为A的。
替换后:
1 2 3 4 5 6 7 8 9 10 { "key": "a804f297-6da8-4454-af7b-6e1e0487329d", "value": { "id": "a804f297-6da8-4454-af7b-6e1e0487329d", "type": "mnemonic", "encryptedData": "{\"data\":\"/9BE9M2LHza8Vofd2iC2O+4ySsg+H7ujPot4RhjF3RTKR3D/rfLUwFAj+kXGq1qw1gP1zLoEFMKr0ANsTyM89IzeVcP0UMYl3qXZ5eHcNclfkaIc8TH7rC7O0VAcSY6dLiHQAIg7GL2mQFjpjZZSxnfqIa+YVkCU989G7JEmyWyJLZuAWXSdhtgVUZWpkJiECuZ43QDBnwSfVi14k2tFnL9YH2FjQ4S1wEhIm33lV9JK0w/fwknVQTXA1DzHUx64uQgtLguA20wzFor3SeCRxIr4HfY27Q==\",\"iv\":\"Kv0KYLadL5RaOmzYeuRQOA==\",\"salt\":\"9x+odK8Pq0o9Q0eNyT8A6/pRrva1DwRtWyNOGm4OSy8=\"}", "sourceHash": "37539da3c74a9d00e32e508450f3cad728ddde84da3e07a16aed0ce66287a8ac", "createdAt": 1703176709184 } }
我们发现使用攻击者A的sui钱包密码test666666.可以打开B的钱包并且查看记助词
注意:这个encryptedData
是会发生改变的,替换的应该为攻击者A刷新后也就是最新的data!
同时需要钱包已经解锁过。
代码分析 wallet\src\ui\app\hooks\useExportPassphraseMutation.tsx:
1 2 3 4 5 6 7 8 9 10 export function useExportPassphraseMutation ( ) { const backgroundClient = useBackgroundClient (); return useMutation ({ mutationKey : ['export passphrase' ], mutationFn : async (args : MethodPayload <'getAccountSourceEntropy' >['args' ]) => entropyToMnemonic ( toEntropy ((await backgroundClient.getAccountSourceEntropy (args)).entropy ), ).split (' ' ), }); }
此方法用于导出密码。
wallet\src\shared\utils\bip39.ts:
1 2 3 4 5 6 7 8 9 10 export function entropyToMnemonic (entropy: Uint8Array ): string { return bip39.entropyToMnemonic (entropy, wordlist); }
这里是 使用英语单词表将熵用于转换为助记符。
wallet\src\background\account-sources\MnemonicAccountSource.ts:
1 2 3 4 5 6 7 8 9 10 async getEntropy (password?: string ) { let data = await this .getEphemeralValue (); if (password && !data) { data = await this .#decryptStoredData (password); } if (!data) { throw new Error (`Mnemonic account source ${this .id} is locked` ); } return data.entropyHex ; }
我们可以很明显地看出钱包用函数 getEntropy
来获取熵。
使用 getEphemeralValue
函数(异步函数)从会话存储中检索熵。
如果没有熵数据存在,那么将尝试解密 indexedDB 数据,使用 decryptStoredData
并获取熵。
1 2 3 4 5 6 7 8 9 10 async unlock (password: string ) { await this .setEphemeralValue (await this .#decryptStoredData (password)); await setupAutoLockAlarm (); accountSourcesEvents.emit ('accountSourceStatusUpdated' , { accountSourceID : this .id }); } ------------------------------------------------------------------------------ async #decryptStoredData (password: string ) { const { encryptedData } = await this .getStoredData (); return decrypt<DataDecrypted >(password, encryptedData); }
不经过解锁的则无法通过这一攻击行为
1 2 3 async isLocked ( ) { return (await this .getEphemeralValue ()) === null ; }
对于sourceHash()
1 2 3 4 5 6 7 8 9 10 11 get sourceHash () { return this .getStoredData ().then (({ sourceHash } ) => sourceHash); } async verifyRecoveryData (entropy: string ) { const newEntropyHash = bytesToHex (sha256 (toEntropy (entropy))); if (newEntropyHash !== (await this .sourceHash )) { throw new Error ("Wrong passphrase, doesn't match the existing one" ); } return true ; }
sourceHash
是对助记词的熵进行哈希处理后的值,在创建新的 MnemonicAccountSource
时,会计算助记词的哈希作为 sourceHash
,并保存到数据库中。
那么可以得到关系:
sourceHash
主要用于验证输入的密码是否正确。在 verifyRecoveryData
方法中,它会将用户提供的助记词熵进行哈希,然后与存储在数据库中的 sourceHash
进行比较,以确保用户提供的助记词熵正确。
decryptStoredData
方法在解密数据时,返回的解密后的数据中包含了 sourceHash
。这有助于在解锁账户来源时,通过验证解密后的 sourceHash
与存储在数据库中的 sourceHash
是否匹配,来确保解密的数据与数据库中的数据一致。
所以这里我们修改了IndexedDB中sourceHash和encryptedData数据 ,但是会话依然存在,所以它会直接从会话中获取熵数据,因为会话没有改变,所以它仍然是受害者的熵数据,唯一改变的就是Lock!。
参考:
[0day]Multiple wallets can leak the users Private key