Biovault - encrypted subdermal data storage

AES-256 encrypted storage on an implanted microchip

Biovault

About:

With the two helper scripts in this repo it is possible to read and write an AES-256 encrypted file on an NFC implant (specifically the xSIID). The hf_i2c_plus_2k_utils script can also be used standalone to write arbitrary data to user memory on a sector of your choosing (sector 0 or 1).

The vault.py script is a python wrapper around hf_i2c_plus_2k_utils which reads and writes the encrypted file (CSV format) to/from the implant. In read mode the CSV file is carved from a user memory hexdump, reversed with xxd, decrypted with openssl (if you have the password) and then displayed in the terminal in JSON format.

vault.py writes data to sector 1 not sector 0 for two reasons:

  1. Sector 0 can still be used to read and write NDEF records. Sector 1 remains untouched when modifying sector 0.
  2. Sector 1 is not accessible from Android or IOS without a custom application or a tool to send raw commands.

Even with encrypted data written to sector 1, when the implant is read from a device such as a phone it will still only return the NDEF record in sector 0 (URL, vcard etc). When the encrypted data needs to be accessed just use the proxmark3 to access sector 1 using vault.py.

Datasheet:

NTAG I2C Plus 2k

To Do:

Requirements:

Software: python3, openssl, jq ,csvtojson

Hardware: proxmark3

Usage:

  1. move hf_i2c_plus_2k_utils.lua to ~/.proxmark3/luascripts/
    • this script is also in the Proxmark3 Iceman fork so you can just do a git pull to grab the latest version
  2. install jq and csvtojson : brew install jq ; npm -g install csvtojson
  3. create a csv file in the following format and save it as vault.txt in the same folder as vault.py:

Example vault.txt:

d,u,p
google.com,testuser,Password1
reddit.com,reddituser,Password2

Write the encrypted file to the xSIID:

  1. python3 biovault.py -m w

Zero sector 1 with null bytes and then write the encrypted file to the xSIID:

  1. python3 biovault.py -m w -z

Dump, carve, decrypt and read the stored file:

  1. python3 biovault.py -m r

Note:

You will need to modify variables pm3_path and uid in vault.py (lines 13,14) to reflect the path to the pm3 binary and your implants UID. If you already have data on sector 1, use the -z flag to zero out the user memory of sector 1 with NULL bytes.

Demo:


hf_i2c_plus_2k_utils.lua

local getopt = require('getopt')
local lib14a = require('read14a')
local cmds = require('commands')
local utils = require('utils')
local ansicolors  = require('ansicolors')

--- Commands
NTAG_I2C_PLUS_2K = '0004040502021503C859'
GET_VERSION = '60'
SELECT_SECTOR_PKT1 = 'C2FF'
SELECT_SECTOR0_PKT2 = '00000000'
SELECT_SECTOR1_PKT2 = '01000000'
READ_BLOCK = '30'
WRITE_BLOCK = 'A2'
ACK = '0A'
NAK = '00'
---


--- Arguments
copyright = ''
author = 'Shain Lakin'
version = 'v1.0.0'
desc =[[

This script can be used to read blocks, write blocks, dump sectors, 
or write a files hex bytes to sector 0 or 1 on the NTAG I2C PLUS 2K tag.

]]

example =[[

    Read block 04 from sector 1:
    script run hf_ntagi2c_plus2k -m r -s 1 -b 04
    
    Write FFFFFFFF to block A0 sector 1:
    script run hf_ntagi2c_plus2k -m w -s 1 -b A0 -d FFFFFFFF

    Dump sector 1 user memory to console and file:
    script run hf_ntag12c_plus2k -m d -s 1

    Write a files hex bytes to sector 1 starting at block 04:
    script run hf_ntagi2c_plus2k -m f -s 1 -f data.txt

]]
usage = [[

    Read mode: 
    script run hf_ntagi2c_plus2k -m r -s <sector> -b <block (hex)>
    
    Write mode: 
    script run hf_ntagi2c_plus2k -m w -s <sector> -b <block (hex)> -d <data (hex)>
    
    Dump mode:
    script run hf_ntagi2c_plus2k -m d -s <sector>
    
    File mode:
    script run hf_ntagi2c_plus2k -m f -s <sector> -f <file>

]]
arguments = [[
    -h      this help
    -m      mode (r/w/f)
    -b      block (hex)
    -f      file
    -s      sector (0/1)
    -d      data (hex)
]]
---


--- Help function
local function help()

    print(copyright)
    print(author)
    print(version)
    print(desc)
    print(ansicolors.cyan..'Usage'..ansicolors.reset)
    print(usage)
    print(ansicolors.cyan..'Arguments'..ansicolors.reset)
    print(arguments)
    print(ansicolors.cyan..'Example usage'..ansicolors.reset)
    print(example)

end
---


--- Message function
local function msg(string)
    print(ansicolors.magenta..string.rep('-',29)..ansicolors.reset)
    print(ansicolors.cyan..string..ansicolors.reset)
    print(ansicolors.magenta..string.rep('-',29)..ansicolors.reset)
end
---


--- Error handling
local function warn(err)

    print(ansicolors.magenta.."ERROR:"..ansicolors.reset,err)
    core.clearCommandBuffer()
    return nil, err

end
---


--- Setup tx/rx
local function sendRaw(rawdata, options)

    local flags = lib14a.ISO14A_COMMAND.ISO14A_NO_DISCONNECT
                + lib14a.ISO14A_COMMAND.ISO14A_RAW
                + lib14a.ISO14A_COMMAND.ISO14A_APPEND_CRC

    local c = Command:newMIX{cmd = cmds.CMD_HF_ISO14443A_READER,
                arg1 = flags,
                arg2 = string.len(rawdata)/2,
                data = rawdata}

    return c:sendMIX(options.ignore_response)
end
---


--- Function to connect 
local function connect()
    core.clearCommandBuffer()

    info, err = lib14a.read(true, true)
    if err then
        lib14a.disconnect()
        return error(err)
    else
        return info.uid
    end
    core.clearCommandBuffer()

end
---


--- Function to disconnect
local function disconnect()
    core.clearCommandBuffer()
    lib14a.disconnect()
end
---


--- Function to get response data
local function getResponseData(usbpacket)

    local resp = Command.parse(usbpacket)
    local len = tonumber(resp.arg1) * 2
    return string.sub(tostring(resp.data), 0, len);

end
---


--- Function to send raw bytes
local function send(payload)

    local usb, err = sendRaw(payload,{ignore_response = false})
    if err then return warn(err) end
    return getResponseData(usb)

end
---


--- Function to select sector
local function select_sector(sector)
    send(SELECT_SECTOR_PKT1)
    if sector == '0' then
        send(SELECT_SECTOR0_PKT2)
    elseif sector == '1' then
        send(SELECT_SECTOR1_PKT2)
    end
end
---


--- Function to write file to sector
local function filewriter(file,sector)
    file_bytes = utils.ReadDumpFile(file)
    len = string.len(file_bytes) / 8
    start_char = 1
    end_char = 8
    block_counter = 4
    -- NTAG_I2C_PLUS_2K:SECTOR_0:225,SECTOR_1:255
    end_block = 225 
    connect()
    select_sector(sector)
    for count = 1, len do
        block = file_bytes:sub(start_char, end_char)
        data = send(WRITE_BLOCK..string.format("%02x",block_counter)..block)
        print('[*] Writing bytes '..block..' to page '..string.format("%02x", block_counter))
        if data == ACK then
            print(ansicolors.cyan..'[*] Received ACK, write successful'..ansicolors.reset)
        else
            print(ansicolors.magenta..'[!] Write failed'..ansicolors.reset)
        end
        start_char = start_char + 8
        end_char = end_char + 8
        block_counter = block_counter + 1
        if block_counter == end_block then
            print(ansicolors.magenta..'[!] Not enough memory space!'..ansicolors.reset)
            break
        end
    end
    disconnect()
end
---


--- Function to dump user memory to console and disk
local function dump(sector,uid)
    connect()
    select_sector(sector)
    counter = 0
    dest = uid..'.hex'
    file = io.open(dest, 'a')
    io.output(file)
    print("\n[+] Dumping sector "..sector.."\n")
    print(ansicolors.magenta..string.rep('--',16)..ansicolors.reset)
    for count = 1, 64 do
        result = send(READ_BLOCK..string.format("%02x", counter))
        print(ansicolors.cyan..result:sub(1,32)..ansicolors.reset)
        io.write(result:sub(1,32))
        counter = counter + 4
    end
    io.close(file)
    print(ansicolors.magenta..string.rep('--',16)..ansicolors.reset)
    print("\n[+] Memory dump saved to "..uid..".hex")
    disconnect()
end
---


--- Function to read and write blocks
local function exec(cmd, sector, block, bytes)
    connect()
    select_sector(sector)
    if cmd == READ_BLOCK then
        data = send(cmd..block)
        msg(data:sub(1,8))
    elseif cmd == WRITE_BLOCK then
        if bytes == 'NOP' then
            err = '[!] You need to pass some data'
            warn(err)
            print(usage)
            do return end
        else
            data = send(cmd..block..bytes)
            if data == ACK then
                print(ansicolors.cyan..'[+] Received ACK, write succesful'..ansicolors.reset)
            elseif data ~= ACK then
                print(ansicolors.magenta..'[!] Write failed'..ansicolors.reset)
            end
        end
    end
    disconnect()
    return(data)
end
---


--- Main
local function main(args)

    for o, a in getopt.getopt(args, 'm:b:s:d:f:h') do
        if o == 'm' then mode = a end
        if o == 'b' then block = a end
        if o == 's' then sector = a end
        if o == 'd' then bytes = a end
        if o == 'f' then file = a end
        if o == 'h' then return help() end
    end

    uid = connect()

    connect()
    version = send(GET_VERSION)
    disconnect()

    if version == NTAG_I2C_PLUS_2K then

        if mode == 'r' then
            print('\n[+] Reading sector '..sector..' block '..block)
            exec(READ_BLOCK,sector,block,bytes)
        elseif mode == 'w' then
            print('\n[+] Writing '..bytes..' to sector '..sector..' block '..block)
            exec(WRITE_BLOCK,sector,block,bytes)
        elseif mode == 'f' then
            filewriter(file,sector)
        elseif mode == 'd' then
            dump(sector,uid)
        end
    
    else
        return print(usage)
    end

    if command == '' then return print(usage) end

end
---


main(args)

biovault.py

#!/usr/bin/python3

import os
import argparse
from threading import Thread
from itertools import cycle
from shutil import get_terminal_size
from time import sleep
from subprocess import PIPE, Popen

# Author: Shain Lakin

pm3_path = "/Users/shain/Documents/tools/proxmark3/"
uid = "0478A5D2CD5280"
pre = '0' * 32

banner = """

"""


class Loader:
    def __init__(self, desc="Loading...", end="[+] Communicating with proxmark ... ", timeout=0.1):
        """
        A loader-like context manager

        Args:
            desc (str, optional): The loader's description. Defaults to "Loading...".
            end (str, optional): Final print. Defaults to "Done!...".
            timeout (float, optional): Sleep time between prints. Defaults to 0.1.
        """
        self.desc = desc
        self.end = end
        self.timeout = timeout

        self._thread = Thread(target=self._animate, daemon=True)
        self.steps = ["⢿", "⣻", "⣽", "⣾", "⣷", "⣯", "⣟", "⡿"]
        self.done = False

    def start(self):
        self._thread.start()
        return self

    def _animate(self):
        for c in cycle(self.steps):
            if self.done:
                break
            print(f"\r{self.desc} {c}", flush=True, end="")
            sleep(self.timeout)

    def __enter__(self):
        self.start()

    def stop(self):
        self.done = True
        cols = get_terminal_size((80, 20)).columns
        print("\r" + " " * cols, end="", flush=True)
        print(f"\r{self.end}", flush=True)

    def __exit__(self, exc_type, exc_value, tb):
        self.stop()


# Parse arguments
parser = argparse.ArgumentParser(description="", \
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-m", "--mode", type=str, default="r", help="Read/Write to vault")
parser.add_argument("-z", "--zero", action='store_true',  help="Zero sector with null bytes" )
args = parser.parse_args()

# Static strings
zero = f"{pm3_path}pm3 -c \'script run hf_i2c_plus_2k_utils -s 1 -m f -f zero.null\'"
aes_enc = f"openssl aes-256-cbc -salt -pbkdf2 -in vault.txt -out vault.txt.enc"
write_vault = f"{pm3_path}pm3 -c \'script run hf_i2c_plus_2k_utils -s 1 -m f -f vault.txt.enc\'"

dump_vault = f"{pm3_path}pm3 -c \'script run hf_i2c_plus_2k_utils -s 1 -m d\' >/dev/null 2>&1"
extract = f"/bin/cat {uid}.hex | awk -F \'{pre}\' \'{{print $2}}\' > dump.bin"
reverse_hex = "xxd -r -ps dump.bin > vault.txt.enc"
aes_dec = "openssl aes-256-cbc -d -pbkdf2 -in vault.txt.enc -out vault.txt.dec"
display = "csvtojson vault.txt.dec | jq"


# Process function
def proc(cmd):
    try:
        proc = Popen(f"{cmd}".split(), \
            stdin=PIPE, stdout=PIPE, stderr=PIPE)
        proc.communicate()
    except KeyboardInterrupt:
        exit(0)


# Create null byte file
def zero_file():
    with open(f"zero.null", "w+b") as z:
        z.write(b"\0" * 3000)


# Delete files
def clean():
    if args.mode == 'w':
        os.remove("vault.txt")
        os.remove("vault.txt.enc")
        if args.zero:
            os.remove("zero.null")
    elif args.mode == 'r':
        os.remove(f"{uid}.hex")
        os.remove("dump.bin")
        os.remove("vault.txt.enc")
        os.remove("vault.txt.dec")

# Loading function
def wait():
    loader = Loader("[+] Place proxmark on implant .. sleeping for 10").start()
    sleep(10)
    loader.stop()
    print("[+] Done ...")


def main():
    try:
        if args.mode == 'r':
            wait()
            os.system(dump_vault)
            os.system(extract)
            os.system(reverse_hex)
            proc(aes_dec)
            os.system(display)
            clean()
        elif args.mode == 'w':
            if args.zero:
                wait()
                zero_file()
                os.system(zero)
            proc(aes_enc)
            wait()
            os.system(write_vault)
            clean()
    except Exception as e:
        print(e)
        exit(0)

main()
******
Written by Shain Lakin on 22 November 2022