Pulse Secure (Ivanti) VPN: Kernel Decryption for Investigation

OctopantagooseOctopantagoose
9 min read

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 with lsblk.

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/loop1when viewed with lsblk.

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 read 1M × 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) and 8, 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 :

CurrentRollbackDirectory 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 the
mount -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


10
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.