钱包安全:[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
}
}
]
image-20231221234355673

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的钱包并且查看记助词

image-20231222011646358

注意:这个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
/**
* Converts entropy (byte array) to mnemonic using the english wordlist.
*
* @param entropy Uint8Array
*
* @return the mnemonic as string
*/
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 来获取熵。

  1. 使用 getEphemeralValue 函数(异步函数)从会话存储中检索熵。
  2. 如果没有熵数据存在,那么将尝试解密 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