Pulse Secure (Ivanti) VPN: Kernel Decryption for Investigation

Table of contents
- Pulse Secure Appliance (PSA):
- Introduction
- Step 1: Decryption Preparation
- Step 2: Checking Collected Evidence
- Step 3: Check if Physical Drive Successfully Mounted
- Step 4: Extracting keys from first 3 partitions
- Step 5: Initial Testing on Partition Segment Using Decryption Key
- Step 6: Ready for Full Image Decryption
- Step 7: Attaching and Mounting the Image for Investigation

Pulse Secure Appliance (PSA):
Version: 9.1.x
Models: PSA300, PSA3000(-V), PSA5000(-V), PSA7000(-V)
Introduction
There are two types of Ivanti Connected Secure Appliances, both of which utilize full-disk encryption. The first type uses LILO bootloaders, while the second uses GRUB bootloaders.
The PSA-Series models we’re going to discuss uses LILO Bootloaders. If you’re interested in kernel decryption with the ISA-Series models that uses GRUB bootloaders, please check this blog instead.
Step 1: Decryption Preparation
A Virtual Machine, which I’ll be using Kali for demonstration.
Step 2: Checking Collected Evidence
Ivanti evidence can be collected by copying the disk image using the dd
command. The only difference is whether the image is saved as a single file or split into smaller slices.
Step 2-1: Single File Evidence
If it’s just a single image.dd
file, we can mount it directly as a physical drive into our Kali environment. Shown in the figure below as Hard Disk 2 (SCSI) and Hard Disk3 (SCSI).
Step 2-2: Multiple Sliced-File Evidence
If the evidence has been split into multiple files, they typically have a .001
extension.
First, use FTK Imager to load the split image files and reconstruct the disk image. Once that's done, we can mount it in Kali as shown in Step 2-1.
Remember to change these settings on FTK Imager:
Mount Type: Physical Only
Mount Method: Block Device / Writable
Step 3: Check if Physical Drive Successfully Mounted
Use lsblk
to check if physical drive successfully mounted.
┌──(kali㉿kali)-[/mnt]
└─$ lsblk
Step 3-1: Successfully mounted
As a verification step, open the image in FTK Imager and compare the number of detected partitions with the output of lsblk
. If both show 14 partitions, the result is correct (Mounted drive is the sdb
shown below). Once the drive is successfully mounted, three volumes are expected to appear on the desktop. These volumes belong to the first three partitions of the image file.
Click on all three volumes and type lsblk
again, we will see that the “Mountpoints” appeared at the end of sdb1
, sdb2
, and sdb3
.
By mounting it as physical drive, you’ll see something like
sdb
/sdc
when viewed withlsblk
.
Step 3-2: Unsuccessfully mounted
If it doesn’t appear as expected, we may need to manually mount the image via a shared folder.
After setting up a Share Folder, we can check it in Kali using:
┌──(kali㉿kali)-[/mnt]
└─$ vmware-hgfsclient
Mount the share folder with the following command (uid=1000
refers to the kali account, reference):
┌──(kali㉿kali)-[/mnt]
└─$ sudo vmhgfs-fuse .host:/ /mnt/ -o allow_other -o uid=1000
aaaaa-node1
┌──(kali㉿kali)-[/mnt]
└─$ ls /mnt/ -alt
total 13
dr-xr-xr-x 1 root root 4192 Apr 13 2025 .
drwxr-xr-x 3 root root 4096 Apr 13 01:58 ..
drwxrwxrwx 1 root root 4096 Apr 11 10:48 aaaaa-node1
Once mounted, use losetup
to associate this file with a loop device. And confirm it again with lsblk
.
┌──(kali㉿kali)-[/mnt/aaaaa-node1]
└─$ sudo losetup -fP aaaaa-node1.001
[sudo] password for kali:
┌──(kali㉿kali)-[/mnt/aaaaa-node1]
└─$ lsblk
By mounting it through share folder, you’ll see something like
loop0
/loop1
when viewed withlsblk
.
Step 4: Extracting keys from first 3 partitions
Now we’re ready to extract the disk images for analysis.
Let’s first create three directories to serve as mount points for partitions extracted from the disk image.
┌──(kali㉿kali)-[/mnt/aaaaa-node1]
└─$ sudo mkdir /mnt/p{1..3}
┌──(kali㉿kali)-[/mnt/aaaaa-node1]
└─$ ls /mnt/
aaaaa-node1 p1 p2 p3
Then mount the partitions one by one:
┌──(kali㉿kali)-[/mnt/aaaaa-node1]
└─$ sudo mount /dev/loop0p1 /mnt/p1
mount: /mnt/p1: WARNING: source write-protected, mounted read-only.
┌──(kali㉿kali)-[/mnt/aaaaa-node1]
└─$ sudo mount /dev/loop0p2 /mnt/p2
mount: /mnt/p2: WARNING: source write-protected, mounted read-only.
┌──(kali㉿kali)-[/mnt/aaaaa-node1]
└─$ sudo mount /dev/loop0p3 /mnt/p3
mount: /mnt/p3: WARNING: source write-protected, mounted read-only.
Reason why we only extract the first 3 loop0s (click to expand):
loop0p1
, loop0p2
, and loop0p3
are the only partitions that are not encrypted. Furthermore, Ivanti system contains two versions: (1) current version and (2) rollback version; and loop0p2
and loop0p3
contains decryption keys for rest of the current/rollback versions.Keys are located in partition #2 and #3, one for the “current” version and one for the “rollback” version.
Through experience, we know that p1
is grub
(probably the page that shows whether if we want to do factory reset or run certain version). And that the kernel
under p2
and p3
are the keys for decypting the “current” and “rollback” partitions. This is what help us create the "decryption key" for the rest of the ivanti’s disk partitions.
Here, we would start by using an open-source shell script: extract-vmlinux, which allows us to extract an uncompressed kernel image (vmlinux) from a compromised kernel image. Download this and continue on with the following commands.
┌──(kali㉿kali)-[/tmp]
└─$ sh ./extract-vmlinux.sh kernel > key1m #with p2's `kernel`
┌──(kali㉿kali)-[/tmp]
└─$ sh ./extract-vmlinux.sh kernel > key2m #with p3's `kernel`
#remember to use `cp`, you don't want to accidentally overwrite things
┌──(kali㉿kali)-[/tmp]
└─$ file key1m
key1m: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=887d6xxxxxxe9b2edb0xxxxxxxxxx44901126fda, stripped
The extracted kernel image contains the key in a specific location, we can get it through locating the Linux Version
’s address:
┌──(kali㉿kali)-[/tmp]
└─$ strings -t x key1m | grep "Linux version "
77a0c0 Linux version 2.6.32-00049-gb2a3a5b-dirty (slt_ec_builder@lxc-linux64-0001-scl6_4_R3_1_9-pulse6_4R3_1_9) (gcc version 4.7.0 20120302 (Red Hat 4.7.0-0.11.1) (GCC) ) #1 SMP Wed Oct 9 15:54:43 EDT 2024
┌──(kali㉿kali)-[/tmp]
└─$ echo $((0x77a0c0+0xd0))
7840144
Undergo same process for both key1m
and key2m
. With the calculated location with the extracted address, we can now get the key:
┌──(kali㉿kali)-[/tmp]
└─$ xxd -s 7840144 -l 16 -p key1m
26589975d3355b2ddcb2b98a438b377b
┌──(kali㉿kali)-[/tmp]
└─$ xxd -s 7840144 -l 16 -p key2m
b70ce5d9a578a0cc382a5e05d68e3644
Step 5: Initial Testing on Partition Segment Using Decryption Key
Before actually decrypting the entire disk image, it’s best practice to do some trial and errors.
Because decryption can sometimes take a significant amount of time, it's recommended to first test which key matches with which partitions. Once identified, the process can be automated to decrypt the rest.
Use the dd
command to take only the first 1 MB for testing out:
┌──(kali㉿kali)-[/tmp]
└─$ sudo dd if=/dev/sda6 of=/tmp/sda6_10 bs=1M count=1
Explanation for the command above:
bs=1M
= block size of 1 megabyte
count=1
= read only 1 block
So the total block size of this command will read1M × 1 = 1MB
from the beginning of the device.
Download Decryption Script (tool)
Github: Pantagoose/ivanti-decrypt
Purpose: Decrypts the encrypted partitions
Usage:
./main <input_file> <output_file> <aes_key_hex>
Tryout each partition with different keys using the above main.go
, ex:
┌──(kali㉿kali)-[/tmp]
└─$ ~/main /dev/sdb6 /tmp/sdb6_10.dec 26589975d3355b2ddcb2b98a438b377b
┌──(kali㉿kali)-[/tmp]
└─$ ~/main /dev/sdb8 /tmp/sdb8_10.dec b70ce5d9a578a0cc382a5e05d68e3644
To check if it has been successfully decrypted, we’ll see the before-and-after as follows:
┌──(kali㉿kali)-[/tmp]
└─$ hexdump -C -n 0x20 /tmp/sdb6_10 # before decryption
00000000 42 47 37 44 3e 5a de 6e 76 c5 ba e6 c0 92 45 d8 |BG7D>Z.nv.....E.|
00000010 9c e5 9b 83 8f 4a f5 f1 a2 dd 68 72 5a f7 60 b7 |.....J....hrZ.`.|
00000020
┌──(kali㉿kali)-[/tmp]
└─$ ./dsmain -d /tmp/p6 /tmp/sdb6_10.dec 26589975D3355B2DDCB2B98A438B377B
┌──(kali㉿kali)-[/tmp]
└─$ hexdump -C -n 0x20 /tmp/sdb6_10.dec # after SUCCESSFULLY decrypted
00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00000020
If you do not see 0000000…
, try it again with another key.
As we tried out all keys, we’ll figure out this pattern:
Partition 1 39MB grub
Partition 2 39MB bootA # 26589975d3355b2ddcb2b98a438b377b (current)
Partition 3 39MB bootB # b70ce5d9a578a0cc382a5e05d68e3644 (rollback)
Partition 5 6000MB # factory-reset
Partition 6 5004MB A # current
Partition 7 35000MB A # current
Partition 8 5004MB B # rollback
Partition 9 35000MB B # rollback
Partition 10 4000MB # (not encrypted) swap
Partition 11 36005MB # (not encrypted) unsure what this is, but contains strings
Partition 12 1001MB A # current
Partition 13 1001MB B # rollback
Partition 14 39MB
Note how
6, 7, 12 (current)
and8, 9, 13 (rollback)
are two different batches that used different keys.
Step 6: Ready for Full Image Decryption
Note that the full image is 1 TB
, ensure that the destination has sufficient space to accommodate it. Due to limited disk space on Kali, we mounted a separate shared folder (name: VPN_image
) to redirect the decrypted image output to the host system.
# Usage: ./main <input_file> <output_file> <aes_key_hex>
┌──(kali㉿kali)-[~]
└─$ ./main /dev/sdb6 /mnt/VPN_image/sdb6.img 26589975d3355b2ddcb2b98a438b377b
Since the process may take a while, we can write a script that automatically decrypts the partitions once executed. Following is an example of the script (decrypt.sh)
:
# Usage: ./main <input_file> <output_file> <aes_key_hex>
┌──(kali㉿kali)-[~]
└─$ cat decrypt.sh
/home/kali/main /dev/sdb6 /mnt/VPN_image/sdb6.img 26589975d3355b2ddcb2b98a438b377b
/home/kali/main /dev/sdb7 /mnt/VPN_image/sdb7.img 26589975d3355b2ddcb2b98a438b377b
/home/kali/main /dev/sdb12 /mnt/VPN_image/sdb12.img 26589975d3355b2ddcb2b98a438b377b
/home/kali/main /dev/sdb8 /mnt/VPN_image/sdb8.img b70ce5d9a578a0cc382a5e05d68e3644
/home/kali/main /dev/sdb9 /mnt/VPN_image/sdb9.img b70ce5d9a578a0cc382a5e05d68e3644
/home/kali/main /dev/sdb13 /mnt/VPN_image/sdb13.img b70ce5d9a578a0cc382a5e05d68e3644
┌──(kali㉿kali)-[~]
└─$ chmod +x decrypt.sh
┌──(kali㉿kali)-[~]
└─$ sudo ./decrypt.sh
Password:
Decrypting an image file
~ 1 TB
may take around half a day, depending on system performance.
Step 7: Attaching and Mounting the Image for Investigation
Now we have our decrypted image files, we will mount it by:
Step 7-1: Attaching Image
First, associates our *.img
with /dev/loop0
by using losetup
. We can again write it as a script (attach.sh
):
┌──(kali㉿kali)-[~]
└─$ cat attach.sh
sudo losetup -fP /mnt/VPN_image/sdb6.img
sudo losetup -fP /mnt/VPN_image/sdb7.img
sudo losetup -fP /mnt/VPN_image/sdb12.img
sudo losetup -fP /mnt/VPN_image/sdb8.img
sudo losetup -fP /mnt/VPN_image/sdb9.img
sudo losetup -fP /mnt/VPN_image/sdb13.img
When finishing attaching, let’s take a look at lsblk
once again. It will now be:
Step 7-2: Mounting the attached image
Prepare directories to be used as mount points for the target partitions. This would help with the investigation process.
With past experience, we know that the partitions correspond to these directories :
Current | Rollback | Directory name |
sdx6.img (loop0) | sdx8.img (loop3) | /root |
sdx7.img (loop1) | sdx9.img (loop4) | /data |
sdx12.img (loop2) | sdx13.img (loop5) | /var |
┌──(kali㉿kali)-[/mnt]
└─$ sudo mkdir current rollback
┌──(kali㉿kali)-[/mnt]
└─$ sudo mkdir -p current/{root,data,var} rollback/{root,data,var}
┌──(kali㉿kali)-[/mnt]
└─$ tree .
current/
├── root/
├── data/
└── var/
rollback/
├── root/
├── data/
└── var/
Once the directories are prepared, the image partitions can be mounted.
To preserve the integrity of the evidence, ensure they are mounted in “read-only mode” using themount -o ro
command. Again, let’s write it as a script (mount.sh
):
┌──(kali㉿kali)-[~]
└─$ cat mount.sh
sudo mount -o ro /dev/loop0 /mnt/current/root
sudo mount -o ro /dev/loop1 /mnt/current/data
sudo mount -o ro /dev/loop2 /mnt/current/var
sudo mount -o ro /dev/loop3 /mnt/rollback/root
sudo mount -o ro /dev/loop4 /mnt/rollback/data
sudo mount -o ro /dev/loop5 /mnt/rollback/var
┌──(kali㉿kali)-[~]
└─$ sh mount.sh
And we’re done! :)
.
.
.
.
.
About the Author
Pantagoose - Trying to preserve knowledge but my brain cells aren’t helping anymore, so I’m relying on blogging as my backup memory.
Octo • Pantagoose • Boo
Subscribe to my newsletter
Read articles from Octopantagoose directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Octopantagoose
Octopantagoose
A bit of Security. A bit of Human.