├── Makefile ├── dump_all.sh ├── ds.h ├── keys.c ├── xex.c ├── README.md └── dsdecrypt.c /Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS=-Wall 2 | LDLIBS=-lcrypto 3 | 4 | all: dsdecrypt 5 | 6 | dsdecrypt: dsdecrypt.o keys.o xex.o 7 | -------------------------------------------------------------------------------- /dump_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -z "$1" ] ; then 3 | echo "usage: $0 image-file" 4 | exit -1 5 | fi 6 | mmls -a "$1" |egrep '^[0-9]+:' | while read line 7 | do 8 | slot=$(echo $line|awk '{print $2}') 9 | start=$(echo $line|awk '{print $3}'|sed -e 's/^0*//') 10 | length=$(echo $line|awk '{print $5}'|sed -e 's/^0*//') 11 | echo "Decrypting $slot" 12 | dd if="$1" bs=512 skip=${start} count=${length} | ./dsdecrypt - slot_${slot}.img 13 | done 14 | -------------------------------------------------------------------------------- /ds.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | struct ds_kernel_key { 8 | const char *kernel_version; 9 | uint8_t key[16]; 10 | }; 11 | 12 | extern const struct ds_kernel_key keys[]; 13 | extern const struct ds_kernel_key *keys_end; 14 | extern const int keys_count; 15 | 16 | #define SECTOR_SIZE (512) 17 | #define BUFFER_SECTORS (16 * 1024) 18 | #define BUFFER_SIZE (BUFFER_SECTORS * SECTOR_SIZE) 19 | 20 | void aes_xex_decrypt_sector (AES_KEY *key, uint64_t sector, void *data); 21 | int aes_xex_decrypt_image (AES_KEY *key, uint64_t start_sector, FILE *fp_in, FILE *fp_out); 22 | -------------------------------------------------------------------------------- /keys.c: -------------------------------------------------------------------------------- 1 | #include "ds.h" 2 | 3 | const struct ds_kernel_key keys[] = { 4 | { .kernel_version = "2.6.32-00002-ga546a52-dirty", .key = {0x71, 0x66, 0x08, 0xCF, 0x60, 0x4B, 0xCC, 0x56, 0x65, 0x25, 0xA4, 0x64, 0x62, 0x02, 0x71, 0xB1} }, 5 | { .kernel_version = "2.6.32-0008-g2f6ef39-dirty", .key = {0xD4, 0xA5, 0x7D, 0x2A, 0x87, 0xD3, 0xD2, 0xCE, 0xA2, 0x79, 0x0E, 0xE3, 0xCF, 0x4E, 0x7D, 0xB3} }, 6 | { .kernel_version = "2.6.32-0032-g2005e8d-dirty", .key = {0x29, 0x09, 0x40, 0xA1, 0x74, 0xF1, 0x8E, 0x2B, 0xE9, 0x80, 0xF9, 0xFA, 0x51, 0x72, 0xA7, 0x4B} }, 7 | { .kernel_version = "2.6.32-00076-g06abd49-dirty", .key = {0xC4, 0x5D, 0x3A, 0x66, 0x39, 0xE2, 0x89, 0xD4, 0x89, 0xA5, 0x4A, 0x55, 0xDC, 0xEA, 0x01, 0xDB} }, 8 | { .kernel_version = "2.6.24.7-g5c78c383-dirty", .key = {0x25, 0xc5, 0x1b, 0xdd, 0x9f, 0xa4, 0x14, 0x73, 0x43, 0xd8, 0xb7, 0x6d, 0xc3, 0x95, 0x7e, 0xba} }, 9 | { .kernel_version = "2.6.24.7-g3eac6a00-dirty", .key = {0x79, 0x81, 0x47, 0x7f, 0x10, 0xa3, 0x3c, 0x38, 0x93, 0x8e, 0x8b, 0xd1, 0xe1, 0x6e, 0x97, 0x51} }, 10 | { .kernel_version = "2.6.32-00376-gb32a05c-dirty", .key = {0x35, 0xa9, 0xaf, 0x64, 0x49, 0x65, 0x1c, 0x24, 0x52, 0x39, 0xa1, 0x51, 0xb0, 0x7f, 0x45, 0xdd} }, 11 | { .kernel_version = "2.6.32-00366-gdd3b182-dirty", .key = {0xbc, 0x6e, 0x92, 0x4a, 0xb3, 0xf8, 0x2f, 0x77, 0x53, 0x9f, 0xdd, 0x97, 0xa2, 0x5b, 0xe4, 0xea} }, 12 | { .kernel_version = "2.6.32-00021-g7726646-dirty", .key = {0x42, 0xd3, 0x45, 0x95, 0x46, 0x55, 0x34, 0x22, 0xe1, 0xe8, 0xcd, 0xe7, 0xa8, 0x55, 0xc6, 0xa} }, 13 | }; 14 | const struct ds_kernel_key *keys_end = keys + (sizeof (keys) / sizeof(struct ds_kernel_key)); 15 | const int keys_count = sizeof (keys) / sizeof(struct ds_kernel_key); 16 | -------------------------------------------------------------------------------- /xex.c: -------------------------------------------------------------------------------- 1 | #include "ds.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define IV_SIZE 16 8 | 9 | static void xor (uint8_t *s1, uint8_t *s2, size_t len) { 10 | while (len--) { 11 | *s1++ ^= *s2++; 12 | } 13 | } 14 | 15 | void aes_xex_decrypt_sector (AES_KEY *key, uint64_t sector, void *data) { 16 | uint8_t pre_iv[IV_SIZE], next_iv[IV_SIZE]; 17 | union { 18 | uint8_t iv[IV_SIZE]; 19 | struct { 20 | uint64_t sector, zero; 21 | } elements; 22 | } iv; 23 | #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ 24 | iv.elements.sector = sector; 25 | #elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ 26 | iv.elements.sector = ntohll (sector); 27 | #else 28 | #error "Unsupported endianness." 29 | #endif 30 | iv.elements.zero = 0; 31 | AES_decrypt (iv.iv, pre_iv, key); 32 | for (uint8_t *p = data ; p != data + SECTOR_SIZE ; p += IV_SIZE) { 33 | xor (p, pre_iv, IV_SIZE); 34 | memcpy (next_iv, p, IV_SIZE); 35 | AES_decrypt (p, p, key); 36 | xor (p, iv.iv, IV_SIZE); 37 | memcpy (iv.iv, next_iv, IV_SIZE); 38 | } 39 | } 40 | 41 | int aes_xex_decrypt_image (AES_KEY *key, uint64_t start_sector, FILE *fp_in, FILE *fp_out) { 42 | int return_code = 0; 43 | void *buffer = malloc (BUFFER_SIZE); 44 | if (buffer == NULL ) { 45 | return 0; 46 | } 47 | for (uint64_t sector = start_sector ; ; ) { 48 | size_t sectors = fread (buffer, SECTOR_SIZE, BUFFER_SECTORS, fp_in); 49 | if (sectors == 0) { 50 | if (errno == 0) { 51 | return_code = 1; 52 | break; 53 | } else { 54 | break; 55 | } 56 | } 57 | uint8_t *p = buffer; 58 | for (uint64_t i = sector ; i != sector + sectors ; i++, p += SECTOR_SIZE) { 59 | aes_xex_decrypt_sector (key, i, p); 60 | } 61 | if (fwrite (buffer, SECTOR_SIZE, sectors, fp_out) != sectors) { 62 | break; 63 | } 64 | sector += sectors; 65 | } 66 | free (buffer); 67 | return return_code; 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | This tool decrypts encrypted partitions as used by the LILO based Pulse Secure appliances. These are the olders models, recent ones boot using GRUB and make use of LUKS for disk encryption. 4 | 5 | # Building 6 | 7 | Have OpenSSL installed with development headers. 8 | 9 | ``` 10 | $ make 11 | ``` 12 | 13 | The tool uses deprecated OpenSSL functions causing some warnings to show up. These can be ignored. 14 | 15 | # Usage 16 | 17 | The `dsdecrypt` tool acts on the single partitions. It detects the correct key by decrypting the first sector and checking if it contains all zeroes. 18 | If no valid key is found, no decryption takes place. 19 | You can force decryption by manually specifying a key using `-k`. 20 | 21 | A script called `dump_all.sh` is provided which uses `mmls` (part of sleuthkit) and `dd` to pipe all partitions on an image file through the decrypter. 22 | 23 | ## Dump entire image 24 | 25 | ``` 26 | $ ./dump_all.sh /path/to/pulse-secure.img 27 | ``` 28 | 29 | ## Decrypt single partition 30 | 31 | ``` 32 | $ ./dsdecrypt /path/to/partition.img decrypted_partition.img 33 | ``` 34 | 35 | # Key extraction 36 | 37 | Keys were most easily extracted by using a decompiler tool and checking into the `loop_setup_root` function. 38 | An easy way to locate this function is to allow the kernel to panic on a missing root disk. It will show the `RIP`/`EIP` value in the stacktrace. 39 | Alternativately you can also check out /proc/kallsyms on a running system to get the address for `loop_setup_root` and `DS_KERNEL_AESKEY`. The latter holds the xor-obfuscated key. 40 | 41 | In order to get the decompressed ELF image from the kernel files you can use the `extract-linux` script from the kernel source. 42 | Checking out the `loop_setup_root` function in Ghidra or Binary Ninja will show you the deobfuscated key, they both their constant folding magic. 43 | 44 | An alternative approach to extract keys is to create a disk image of the appliance that can then be run it using qemu. 45 | Qemu should be started with a local monitor (e.g. `-monitor telnet:127.0.0.1:8000`). 46 | After selecting a boot entry connect to the monitor and dump the memory to a file. 47 | You can then use tools like aeskeyfind to extract the key from the memory dump. 48 | 49 | ## Factory reset kernel 50 | 51 | For the factory reset image the `extract-linux` script did not work. Whatever is decompressed is not a valid ELF file. 52 | Also this kernel is a 32-bit build, where the others were x64-64. 53 | 54 | From the x86-64 kernels we know that the key is set in the `loop_setup_root` root function. 55 | So in order to find it we boot the image in QEMU using a drive type not supported by the configuration. 56 | This will cause the kernel to panic inside the loop_setup_root, yielding the instruction pointer at time of crash. 57 | By also enabling the gdb port in QEMU we can attach gdb and use it to inspect the code and memory contents. 58 | 59 | By browing through the code looking for four consecutive xor operations we quickly find the code below: 60 | 61 | ``` 62 | 0xc0236890: mov $0xc049dbac,%esi 63 | 0xc0236895: lea -0x1c(%rbp),%eax 64 | 0xc0236898: mov %eax,%edi 65 | 0xc023689a: movsl %ds:(%rsi),%es:(%rdi) 66 | 0xc023689b: movsl %ds:(%rsi),%es:(%rdi) 67 | 0xc023689c: movsl %ds:(%rsi),%es:(%rdi) 68 | 0xc023689d: movsl %ds:(%rsi),%es:(%rdi) 69 | 0xc023689e: xorl $0x99ed2bf2,-0x1c(%rbp) 70 | 0xc02368a5: xorl $0xaeef41fe,-0x18(%rbp) 71 | 0xc02368ac: xorl $0x141058c7,-0x14(%rbp) 72 | 0xc02368b3: xorl $0xd2ed180e,-0x10(%rbp) 73 | ``` 74 | 75 | This code loads 16 bytes from 0xc049dbac onto the stack and xors them with 4 immediate values. 76 | 77 | Inspecting the 16 bytes loaded onto the stack: 78 | 79 | ``` 80 | (gdb) x/16b 0xc049dbac 81 | 0xc049dbac: 0xd7 0xee 0xf6 0x44 0x61 0xe5 0xfb 0xdd 82 | 0xc049dbb4: 0x84 0x80 0xa7 0x79 0xcd 0x8d 0x93 0x68 83 | ``` 84 | 85 | Manually performing the xor operations we get a key which does indeed decrypt the factory-reset partition, unlocking all its mysteries. 86 | -------------------------------------------------------------------------------- /dsdecrypt.c: -------------------------------------------------------------------------------- 1 | #include "ds.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | static int all_zeroes (uint8_t *buffer, size_t len) { 9 | for (uint8_t *p = buffer ; p != buffer + len ; p++) { 10 | if (*p != 0) { 11 | return 0; 12 | } 13 | } 14 | return 1; 15 | } 16 | static const struct ds_kernel_key* ivanti_probe_key (uint8_t *buffer) { 17 | for (const struct ds_kernel_key *p = keys ; p != keys_end ; p++) { 18 | uint8_t decrypt_buffer[SECTOR_SIZE]; 19 | memcpy (decrypt_buffer, buffer, SECTOR_SIZE); 20 | AES_KEY key; 21 | AES_set_decrypt_key (p->key, 128, &key); 22 | aes_xex_decrypt_sector (&key, 0, decrypt_buffer); 23 | 24 | if (all_zeroes (decrypt_buffer, SECTOR_SIZE)) { 25 | memcpy (buffer, decrypt_buffer, SECTOR_SIZE); 26 | return p; 27 | } 28 | } 29 | return NULL; 30 | } 31 | struct options { 32 | const struct ds_kernel_key *key; 33 | int force, verbose; 34 | const char *input, *output; 35 | }; 36 | static FILE *parse_input_filename (const char *filename) { 37 | if (strcmp (filename, "-") == 0) { 38 | return stdin; 39 | } else { 40 | return fopen (filename, "rb"); 41 | } 42 | } 43 | FILE *parse_output_filename (const char *filename) { 44 | if (strcmp (filename, "-") == 0) { 45 | return stdout; 46 | } else { 47 | return fopen (filename, "wb"); 48 | } 49 | } 50 | int decrypt (struct options *options) { 51 | int result = 0; 52 | FILE *input = parse_input_filename (options->input), *output = NULL; 53 | if (input == NULL) { 54 | fprintf (stderr, "Error opening input: %s\n", strerror (errno)); 55 | return 0; 56 | } 57 | uint64_t start_sector = 0; 58 | if (options->key == NULL) { 59 | if (options->verbose) { 60 | fprintf (stderr, "No key specified, probing.\n"); 61 | } 62 | uint8_t buffer[SECTOR_SIZE]; 63 | if (fread (buffer, SECTOR_SIZE, 1, input) != 1) { 64 | fprintf (stderr, "Short read.\n"); 65 | goto close_in; 66 | } 67 | const struct ds_kernel_key *key = ivanti_probe_key (buffer); 68 | if (key == NULL) { 69 | fprintf (stderr, "No matching key found.\n"); 70 | goto close_in; 71 | } 72 | output = parse_output_filename (options->output); 73 | if (output == NULL) { 74 | fprintf (stderr, "Error opening output: %s\n", strerror (errno)); 75 | goto close_in; 76 | } 77 | if (fwrite (buffer, SECTOR_SIZE, 1, output) != 1) { 78 | fprintf (stderr, "Short write.\n"); 79 | goto close_out; 80 | } 81 | options->key = key; 82 | start_sector = 1; 83 | if (options->verbose) { 84 | fprintf (stderr, "Probe done key=%s start_sector=%lld\n", options->key->kernel_version, start_sector); 85 | } 86 | } else { 87 | output = parse_output_filename (options->output); 88 | if (output == NULL) { 89 | fprintf (stderr, "Error opening output: %s\n", strerror (errno)); 90 | goto close_in; 91 | } 92 | } 93 | AES_KEY key; 94 | AES_set_decrypt_key (options->key->key, 128, &key); 95 | result = aes_xex_decrypt_image (&key, start_sector, input, output); 96 | close_out: 97 | fclose (output); 98 | close_in: 99 | fclose (input); 100 | return result; 101 | } 102 | void usage (void) { 103 | fprintf (stderr, 104 | "usage: [-k key] [-v] \n" 105 | "\tKey\tKernel version\n" 106 | ); 107 | for (int i = 0 ; i != keys_count ; i++) { 108 | const struct ds_kernel_key *key = &keys[i]; 109 | fprintf (stderr, "\t%2d\t%s\n", i, key->kernel_version); 110 | } 111 | exit (-1); 112 | } 113 | int parse_options (int argc, char *argv[], struct options *options) { 114 | static const char *optstring = "k:v"; 115 | 116 | options->key = NULL; 117 | options->force = 0; 118 | options->verbose = 0; 119 | options->input = NULL; 120 | options->output = NULL; 121 | 122 | for (int ch = getopt (argc, argv, optstring) ; ch != -1 ; ch = getopt (argc, argv, optstring)) { 123 | switch (ch) { 124 | case 'k': 125 | { 126 | int key = atoi (optarg); 127 | if (key < 0 || key > keys_count) { 128 | fprintf (stderr, "Invalid key index\n"); 129 | usage (); 130 | } 131 | options->key = &keys[key]; 132 | } 133 | break; 134 | 135 | case 'f': 136 | options->force = 1; 137 | break; 138 | 139 | case 'v': 140 | options->verbose = 1; 141 | break; 142 | 143 | case 'h': 144 | default: 145 | usage (); 146 | return 0; 147 | } 148 | } 149 | switch (argc - optind) { 150 | case 2: 151 | options->input = argv[optind]; 152 | options->output = argv[optind + 1]; 153 | break; 154 | case 1: 155 | options->input = argv[optind]; 156 | options->output = "-"; 157 | break; 158 | case 0: 159 | options->input = "-"; 160 | options->output = "-"; 161 | break; 162 | default: 163 | return 0; 164 | } 165 | return 1; 166 | } 167 | int main (int argc, char *argv[]) { 168 | OPENSSL_init_crypto (0, NULL); 169 | struct options options; 170 | 171 | if (parse_options (argc, argv, &options) == 0) { 172 | exit (-1); 173 | } 174 | decrypt (&options); 175 | } 176 | --------------------------------------------------------------------------------