Tags: qr basic qrcode

Rating: 5.0

> We've found an Acorn Computers machine from the 80's, with 32K of RAM.
> It is working and running BBC Basic, a fantastic programming language.
> ButcherCorp was Acorn's owner and this computer is running one of their projects.
> It is a "QR Code Shell".
> You send a BBC code to it and, if it renders a valid QR code, the QR code's content will be executed in a Linux system shell. There are certain limitations, such as the number of characters. Currently it's only 132 (the code will be truncated at that point). We also know that at read time the colors black and white are swapped, and that the interpreter's background color is black.
>
> Note: due to hardware limitations, test your payloads locally before sending them to be executed on the server.
>
> Server: nc oldschool.pwn2.win 1337

It is a little hard to understand the instructions here (the original version didn't specify "in a Linux system shell", which made it extremely obtuse) but it's clear enough that we send some BBC Basic code which has to render a QR code.
We can play around with an emulated BBC Micro using [JSBeeb](https://github.com/mattgodbolt/jsbeeb) and start reading up on how to draw graphics.
Some [documentation](http://www.riscos.com/support/developers/bbcbasic/part2/simplegraphics.html) tells us how to draw a point:


POINT X,Y


But even a small, version 1 QR code is 21x21 "modules". Even if we could draw each point with a single character, we'd need 441 characters to draw a QR code - and using the POINT command each point takes at least 9 characters.

> Techincally you'd need more than 21x21 since QR codes are supposed to have a border.
> But since we're drawing on a black backround, and inverting colors after so it becomes white, that provides a border for us, and we can skip drawing the border.

The prompt specifies that we can only send 132 characters of code (it isn't immediately obvious if this refers to the BASIC code, or the contents of the QR code, but it turns out to mean the BASIC code.)
So we need to find a more compact way to draw the QR code.

After a _lot_ of skimming through BBC Micro documentation I discovered [Graphics Mode 7](http://www.bbcbasic.co.uk/bbcwin/manual/bbcwinh.html), the [Teletext](https://en.wikipedia.org/wiki/Teletext) compatibility mode.
In this mode, you can't use the graphics drawing APIs but you can draw simple graphics in text mode by using a special chacater set where each character draws a 2x3 pixel array:

![table showing a mapping of character values to 2x3 pixel grids](https://i.imgur.com/afFYmc5.png)

We can use these character codes to draw a QR code much more efficiently than using the POINT command. First we have to send the control character 151 to draw white graphics (and recall that we are drawing white on a black background, and need to draw an _inverted_ QR code, per the description).

> Fortunately it seems that you don't need to actually switch to mode 7 for this technique to work, which saves us some code.


PRINT CHR$(151)CHR$(160)CHR\$(161)...


That's an improvement, but it doesn't get anywhere near 132 characters.
For that, maybe we can just put the raw character values into the source code.
BBC Basic supports putting non-ASCII characters into string literals by typing alt-codes, so maybe it will work?


PRINT"·ó³µµ½µ·ó³µ
µ¯¥µö¼¤µ¯¥µ
£³£ñý­åó³ó±
ûó¤æ¿«äúþé±
ññó³ô¥­¢à±¡
µü´µª½­§®¬¡
õóñµé½êõª¹´"


> I think the encoding is messed up here - I'm printing 8 bit chars, likely being interpreted as Latin-1, but I suspect they got converted to UTF-8 along the way so don't expect copy-and-pasting that to work, but that's what I see dumping it to the terminal.

That's just 91 chars!
But when we paste it into the paste box in JSBeeb, there are two problems:
First we don't see the non-ASCII characters printed.
Based on the documentation, we should actually be able to see the graphcis chars show up as they are "typed" into the system (as we would if we typed them manually with alt-codes).
Second, it seems to stop parsing after the first newline and reports Missing " - I guess multiline string literals are not supported!

For now we can work around the second issue by using one PRINT statement per line:


PRINT"·ó³µµ½µ·ó³µ"
PRINT"µ¯¥µö¼¤µ¯¥µ"
PRINT"£³£ñý­åó³ó±"
PRINT"ûó¤æ¿«äúþé±"
PRINT"ññó³ô¥­¢à±¡"
PRINT"µü´µª½­§®¬¡"
PRINT"õóñµé½êõª¹´"

Unfortunately this bumps us up to 139 chars but we'll pare it down later.
We don't get the syntax error due to the newlines anymore, but the non-ASCII characters still get stripped when pasting into JSBeeb.

Reading the JSBeeb documentation turns up this:

> embedBasic=X - loads 'X' (a URI-encoded string) as text, tokenises it and puts it in PAGE as if you'd typed it in to the emulator

Perfect.
Click that and you should see the BBC Micro draw a lovely inverted-color QR code!
My phone will actually parse it even though the colors are inverted.

Now we just need to pare it down to 132 bytes or less.
I tried all sorts of things to get a newline into the string literal so we can draw the QR code on one line of BASIC and one PRINT statement.
either a carriage return or a newline ends the BASIC line and requires a new PRINT statement.
Other options like form feed work a little better but mess up the drawing (form feed seems to act like a page break and clears the screen).
The lines do autowrap but the tab character only seems to insert one space, and the default graphics mode is 40 characters wide so that won't work.

Eventually I found that PRINT can take multiple comma separated values and will print them at 10-character tabstops. Since a line of our QR code already is 11 characters and passes the first tabstop, we only need to print two extra values per line to auto-wrap to the next line:


PRINT"[first line of QR code]",0,0,"[second line of QR code]",0,0,...


Each line needs its own control character (code 151) to enable the graphics character set.
All together, the code to draw a QR code now comes to 133 characters.
SO CLOSE.
At this point, I tried just stripping the last graphics character from the last line of the QR code.
Since QR codes have error correction built in and the end of the last line is not part of one of the alignment targets, the code remained readable and we made it within 132 bytes!

> Most of the time.
> Occasionally the server says it's not a valid QR code, but after a retry it usually works.

When we send this payload to the server (the payload here is the raw BASIC source, not URL-encoded), it chews on it for a bit while the BBC Micro generates the QR code, then spits back:


Wait, processing...
flag.txt


One more try with the payload cat flag.txt and it spits out the flag.

Python script to generate JSBeeb URLs for testing or to actually connect to the server and run the attack:

python
import qrcode

def encode(contents):
# encode contents in BBC Basic which draws a QR code
qr = qrcode.QRCode(
# smallest size
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
# 1:1 pixels
box_size=1,
# border of 0 techinically is not allowed
# but since we're drawing on a black background we get border for free
# (inverted color so black becomes white)
border=0,
)
qr.make(fit=True)

matrix = qr.get_matrix()

def getpix(a, x, y):
try:
return a[x][y]
except IndexError:
return False

# iterate through QR code bitmap in 2x3 pixel chunks
# find the appropriate teletext graphics character for each chunk
lines = []
for y in range(0, len(matrix[0]), 3):
line = ""
for x in range(0, len(matrix), 2):
grid = [
getpix(matrix, x+0, y+0), getpix(matrix, x+1, y+0),
getpix(matrix, x+0, y+1), getpix(matrix, x+1, y+1),
getpix(matrix, x+0, y+2), getpix(matrix, x+1, y+2),
]

graphic = 0
for i in range(len(grid)):
# we want to draw the QR code with inverted color.
# this seems to do the trick
v = 1 if grid[i] else 0
graphic |= (v << i)

# http://www.riscos.com/support/developers/bbcbasic/part2/teletext.html#TeletextGraphics
line += chr((graphic + 160) if graphic < 32 else (graphic + 192))
lines.append(line)

# Remove the last character to get down to the limit of 132
# This works because QR code has error correction
lines[-1] = lines[-1][:-1]
# to get characters down, instead of a PRINT for each line, we use the
# tabulation feature and printing enough values to fill each line
s = 'PRINT' + ',0,0,'.join([f'"\x97{line}"' for line in lines])

return s

if __name__ == '__main__':
# generate BBC BASIC to show QR code containing command

if True:
# talk to the server for real
from pwn import *

# connect to server
conn = remote('oldschool.pwn2.win', 1337)

# do PoW
conn.recvuntil('Send the solution for "hashcash -mb 25 ')
prompt = conn.recvuntil('":', drop=True).decode('utf-8')
hc = process(['hashcash', '-mb', '25', prompt])
hc.recvuntil('hashcash token: ')
conn.send(hc.recvuntil('\n', drop=True))

conn.interactive()
else:
# test QR code generation locally to avoid hammering the server
import urllib.parse

# show the raw payload for sanity check