WSJT-X SuperFox TOTP: Part 3

I feel a little bad delaying this blog post for so long. A few other things got into the pipeline. The tl;dr is WSJT-X 2.7.0-rc7 is pretty fine. It uses TOTP now and third parties can run their own verification.

In previous posts we looked into the now discontinued SuperFox verification scheme. Since that post rc7 has been released. Many various solutions were proposed with various trade offs. Remember that we are dealing with dozens of bits here, not bytes. The final solution was chosen for its simplicity.

TOTP

One of our first suggestions when digging into alternatives was using a time based one time password (TOTP), similar to the H40WA dxpedition. TOTP works by both parties having a shared secret key. Both parties combine the time and the secret key, then hash the result. This allows verification without sharing the secret key or being able to replay an old message.

At this point you’ve probably spotted a problem however, both parties need the key. The way this is worked around, both with the H40WA system and the WSJT-X system is that the receiving station doesn’t hold the key. Instead a server holds the key and the client can request the final value from the server.

% curl https://www.9dx.cc/check/K1JT.text
2024-11-22T08:49:00Z K1JT 229106
2024-11-22T08:49:30Z K1JT 305435
2024-11-22T08:50:00Z K1JT 806080
2024-11-22T08:50:30Z K1JT 869989
2024-11-22T08:51:00Z K1JT 699026
2024-11-22T08:51:30Z K1JT 250722
2024-11-22T08:52:00Z K1JT 485396
2024-11-22T08:52:30Z K1JT 429621
2024-11-22T08:53:00Z K1JT 450221
2024-11-22T08:53:30Z K1JT 763383
2024-11-22T08:54:00Z K1JT 182034

The important part that lets this work is that the server only return the TOTP values for already transmitted windows. A transmission using an old code won’t have the correct timestamp and the client won’t verify it.

Generally speaking WSJT-X stations have time set pretty well as transmission and reception only really works if clocks are within 1 second. If a client did have their time set in the past an attacker could use old codes, however in practice this is rare and the consequence is minimal.

WSJT-X actually uses the following request to perform checks

% curl https://www.9dx.cc/check/N9ADG/2024-09-05T19:07:00Z/866973.text
2024-09-05T19:07:00Z N9ADG 866973 VERIFIED

The actual code that validates the response is pretty basic and is just checking that it sees VERIFIED at the end

  if (verify_message.endsWith(" VERIFIED")) {
    return QString("%1   0  0.0 %2 ~  %3 verified").arg(rx_time.toString("hhmmss")).arg(hz).arg(callsign);
  } else
    if (verify_message.endsWith(" INVALID"))
    {
      return QString("%1   0  0.0 %2 ~  %3 invalid").arg(rx_time.toString("hhmmss")).arg(hz).arg(callsign);
    }
    else
      return QString{};

Side note here - The way WSJT-X handles data during a lot of the verification steps is passing through strings then checking if they contain certain substrings. I haven’t spotted a trivial way of doing due to the limited nature of an FT8 transmission (no lowercase, short freetext and no new lines) - but I wouldn’t be surprised if there is some fun to be had here. It might be possible to have the client process a crafted line with an old time.

WSJT-X have listened to feedback and allow running of SuperFox without verification keys. Not only that they do have configuration option to change the verification server. An example server might be something like:

from http.server import HTTPServer, BaseHTTPRequestHandler

class MyHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(bytes("meow meow VERIFIED", "utf-8"))

httpd = HTTPServer(('localhost', 8999), MyHandler)
httpd.serve_forever()

Do note that you’ll need to be able to handle many amateur radio operators requesting tokens every 0 and 30 seconds.

It’s really good that the developers have listened to feedback. To recap:

  • Entirely open source
  • No secret sauce
  • Can be used without a third party
  • Can provide own verification service

I did quickly want to touch on some of the downsides of this approach. While the transmitting station doesn’t require an internet connection the receivers do. The server does allow checking of old logs so temporary internet outages can be patched over. I think this is a fair compromise and likely works for 99.9% of users. For the users it doesn’t work for - it just means waiting for a QSL card to confirm that it wasn’t a pirate.

Some other approaches used CSPRNG (Cryptographically secure pseudorandom number generator) stepped backwards. Occasionally the broadcasting station would transmit a signed message with the parameters for the CSPRNG which would allow verifying past messages. This was a pretty cool approach even if complicated.


CODAN Selcall Part 2 - Pagecall Magic

In part one I left you with a cliff hanger regarding page calls. And I particularly like this little note:

This shouldn’t be hard

Like it’s a radio from 1993. Easy right?

That was nearly 7 months ago. That being said I haven’t been working on that project full time and up until recently have only really just been chipping a way at it here and there. In the last few weeks I’ve been using the project as a bit of an escape. Since there’s so many facets to reverse engineering, different pathways and different things to discover the project has provided many simple tasks for me to work on that don’t require much capacity. I can just put some music on then label some functions, decompile code, run emulator tasks. Just poke random memory locations to see what they do.

On Sunday I was able to replicate the magic hashing function used in the page call protocol in Python, and hence why I can write Part 2!

What is page call and why is it different?

Page calls are text messages sent between radios. Essentially SMS for radios. Or I guess WhatsApp(?) messages for radios. The protocol is very similar to the selcall protocol, just with the message spliced in…. however…..

Head unit for Codan 9323 showing a pagecall “rawr nuzzles”

Codan decided that page calls should be locked down. My assumption here is that the 93xx series of radios were the first by Codan to be advanced enough to allow paging and to keep a market edge they wanted to make it harder for competitors to provide interoperability (pretty sad imho). So page calls include two extra bytes that are validated before receiving.

screenshot of code with two bytes being commented as “not sure what these are”
Freeselcall up until now has only been able to receive messages. Messages sent would not be received by radios using the magic bytes.

Surely someones worked it out?

I’m not sure if anyone actually has. I can’t imagine I’m the first but I can’t really find any resources online about these two bytes. I found one groups.io post about someone asking about it - but no responses (shame I can’t find that post anymore! I’d love to respond! I found the post!)

But doesn’t Barrett radio support sending pages? Well some more modern radios do, except they somewhat side stepped the problem by working with Codan.

Screenshot of the intellectual property exchange deed which lists out how codan and barrett are working together

What about Skippy? Well it claims it can send page calls but if you look at the details it can only send them to other Skippy users - likely because of these two bytes not being known. I’d love to know if Skippy to Freeselcall worked.

skippy compatability matrix where barret and ngt can’t receive pagecall
And to be honest, I’m not all that surprised. There’s not a lot of demand of sending messages like this. There’s limited users and the people who do need this functionality will just pay for the radio.

But these two bytes were like holding a red flag to bull for me. I needed to know.

The basics

Apart from doing prior research and asking ex-Codan employees I checked a few things. How do the bytes change when I change the selcall id by 1. What happens when I change it by 1000. What happens if I change the message. What happens if I flip the to and from address.

This data quickly builds up test cases. Lets you validate assumptions. I only ended up with a few of these tests though as it requires a lot of tedious work to do, along with waiting for transmission. But I had enough to rule out basic single XOR or just swapping parts of bytes. It was certainly cascading in some way since changing one bit change a lot. It also told me that since the bytes didn’t change with the message that the bytes weren’t used for confirmation or recovery. Purely as a security check.

Effort to not touch our radio

Even though the radio was designed in 1993, the Codan 9323 is still a 100W HF radio. And a good one. This means they still sell for $400+ on eBay. Buying a replacement one would be annoying and expensive.

I wanted to minimise fucking around and finding out on our radio. No pulling eeproms, no tapping into the memory bus.

Luckily there are dumps of the firmware online now. Bless these people. This allows us to look at just the software side of things. Likewise there’s a bunch of service manuals, PCB and circuit diagrams for many of the units allowing us to paint a picture of how the system works.

At this point I blindly fired up Ghidra and pretended I knew what I was doing. Then I spent about 3 hours reverse engineering a supposed crypto function that turned out to be 16bit long division.

Update

In between writing the first draft of this post and today, I managed to find a bug where a malformed pagecall packet caused our radio to hang and on reboot crash / act odd. I was luckily able to fix this. After trying so hard to ensure I didn’t break out radio that was an extremely close call.

Maybe we should emulate it?

At its core it’s an 8051. Surely there are a ton of of 8051 emulators. I could just dump the ROM into one and go.

Well, that’s partly true. There are a ton of 8051 emulators, however it’s not just the CPU that needs emulating, its all the supporting hardware around it. So out come the schematics, service manuals and we begin collecting datasheets for all the random ICs scattered on the board.

Then you realise that the ROM is 128k, and an 8051 can only support 64k. How does that work? The magic of bank switching. The code will use additional pins to switch in and out banks of memory. The code has to support this and know what its doing. Since this is done in software the emulator needs to support this as well, and everyone does this differently so there isn’t a standard way to emulate the bank switching.

So I ended up starting with py8051, which is a python wrapper for emu8051. I really enjoyed working in Python for this project. I didn’t need speed - just something I could quickly modify to get the job done.

The emulator was super handy in understanding what code was, what functions were doing and allowed me to continue reverse engineering in Ghidra. Eventually the emulator progressed so far in hardware emulation that I was able to get the boot screen to display on a virtual display, and with some memory poking I could even just some of the normal displays to show up.

virtual display of emulated codan 9323 - shows “no channels fitted, lsb, call, rx”

I never got the emulator(s) to a point where they would run normally though.

Emulators? With an s?

Yes. Many of you probably already gathered but our Codan 9323 radio has two parts, a remote head and a base. (Other units are sold with a front panel on the base, however this is effectively the head unit packed inside the base - other fun fact I found out is that you can have multiple head units connected!)

So this poses the question - does the magic get calculated on the base or the head unit.

  • Surely the base unit since the head would just be a dumb interface right?
  • Except you can set a privacy code on the head unit
  • Except maybe it just sends that back to the base unit
  • But maybe the base unit a dumb device
  • But you can also send a page call from the serial port?
  • Maybe the base unit just asks the head unit to encode the packet?

I continued chipping away at both. When I got stuck with one unit and I switched to the other. Was this the fastest way to work. No. Certainly not. But its what I could fit in to random spare gaps in time and days where I didn’t really want to go out to play.

Maybe I should just check?

At this point I had emulated both the bit banged I2C and the hardware based I2C… hold up… hardware based I2C?

Oh yeah this is not a standard 8051, It’s a Philips 80C552. This means there’s a bunch of extra functionality, which is more logic to handle in the emulator sigh.

Anyway, at this point I had emulated all the I2C. Not to the point that the two emulators could talk to each other, but enough for me to send packets by hand and see what happens. Reverse engineering the code gave me an idea of what the packets looked like, but not enough to get anything to work.

base unit of codan 9323 with logic analyser connected to some pins

Eventually I gave in and decided on a safe way to capture the I2C data between the head unit and the base unit. The front panel connector that wasn’t used on our 9323 was perfect for tapping into the I2C between head and base unit.

pulseview decoding the i2c packets

These packet captures done with PulseView were super useful. I could replay the messages in the emulator and see the results. It also revealed very quickly that the me magic bytes were not sent from the head unit to the base unit when sending a page.

My effort focused again on reverse engineering the base unit. But more importantly building out the emulator for the base to a point that I could the page call I2C messages.

With some bugs fixed in the emulator I finally had the emulated base station processing the page call messages. It even tried to transmit them - however I hadn’t emulated the tone generator….

It didn’t matter though because the important part was that it generated the bitstream for the transmission including the secret bytes.

hex editor view showing the pagecall encoded

At this point we have some options

  • Ship a 8051 emulator and a copy of the 9323 base station firmware in Freeselcall
  • Pre-calculate all possible value pairs (this is actually pretty possible, and not tooooo big of a lookup table)
  • Work out the algorithm

The algorithm

I took two approaches. Looking what happens to the emulator step by step, along with labelling and correcting Ghidra decompilation. Some parts I could only work out by watching the emulator. Eventually I came up with a solution that worked for 3 test cases. But failed on the next one - some tweaking and testing and all the test cases that I have recorded now work.

I then hastily set up freeselcall to use the new code and sent a test transmission from the IC7100 to the Codan 9323 and it worked! I then spent about 20 minutes reading the users manual to figure out how to actually open a page call (turns out you need the microphone, then press the recall button then call button). I found it hilarious that I knew how to generate the magic bytes but not know how to open the message on radio itself.

I generated a test file for Mark to transmit and it worked on their NGT radio!

codan ngt with a pagecall message: woof woof woof
Update It’s been been tested against a Codan Envoy!

envoy pagecall: vk5qi selcall test

Fin

Where does this leave us on this epic journey.

  • By the time you are reading this, freeselcall should be released on GitHub (and maybe pypy) with support for pagecalls - if not it will be very soon I promise
  • I specifically haven’t implemented privacy codes or 6 digit pagecalls. I have no use for these. It’s not hard work - but it is work. If someone makes a PR and tests for it I will support it.
  • I now know far too much 8051 assembly and Philips 8051 I2C information than I ever wanted to know
  • I have a copy of all the random IC datasheets now
  • The emulators won’t be released publicly nor the Ghidra annotated project
  • 6 months ago I tagged a series of functions as likely to be related to the pagecall magic - I was right, but I didn’t have a way of working out the inputs/outputs without the emulator

Side quests

Like all good projects there are side quests. I won’t document them here, but to give you a taste of where things lead to:

  • Figured out why the Codan 9323 gets stuck TXing
  • Made a script to colorise Ghidra based on logs
  • Found 3 different 8051 emulation quirks/bugs where my CPU didn’t match the emulators view of the world - these were annoying to troubleshoot. One of which was even present in the commerical Keil 8051 emulator
  • There is a 1-wire IC - DS2401 - that is packaged to look identical to a through hole transistor. It stores the serial number of the unit. I feel like the packaging is specifically to hide where the serial number is so people can’t reprogram / change them? I read ours by sniffing the 1 wire data pin on boot

Simpson Desert trip

Lizard laying on the track. It’s as red as the red sand its laying on

The plan

It’s simple, leave from Purni bore, and arrive at Birdsville… except its not that simple. The Simpson is a criss cross of different “lines”. You have the QAA, Rig Road, French line, K1 line, Madigan and others. Each serving a specific purpose in the past and today offering different challenges.

Your typical crossing picking a straight route across is probably around 3-4 days. This is 3-4 days without access to water, shops, or fuel. We however decided on route that touched on the French, Madigan, QAA and K1. The idea being that we wanted to see a bit of everything along with check out some of the interesting POIs like Poeppel’s corner (the corner of QLD, SA and NT).

map of the track showing a route from mt dare, along french line, north up the colson track, across east along the madigan then down the hayriver track and finally ending on the qaa line in birdsville

This resulted in a plan that was upwards of 12 days and 914km without services. We plan on days being low kilometers to ensure we have enough water and fuel in case something goes wrong or backtracking is required.

Adding to complications was the recent rainfall in the area. We had to keep a close eye on river levels / crossing locations and which properties we could enter and exit through.

In the end we completed the trip with 8 days in the desert. The second half of Hay River track was exceptionally good, and we breezed through the QAA line. A lot faster than the Madigan and French lines.

Permits

There’s a number of permits required when travelling the Simpson. For the Madigan line a Central Land Council entry permit is required as you are travelling through their land. Entry from SA and into Poepell’s corner requires South Australia Desert Parks pass. Camping in the QLD section requires a QLD parks camping permit.

The CLC permit can through very last minute. If we didn’t receive this we would have had to adjust our route. Luckily it was fine.

Australia is big….

Really big. Our plan was to start from Mt Dare to cross the desert, however just getting to Mt Dare from Melbourne is 23 hours of driving and 1,948km. To assist with this we decided to travel a week earlier than expected to Adelaide and start journey from there. This still left 1,237km and 15 hours driving which we split into two - stopping at William Creek over night.

swag setup at william creek in the stars next to the car and camp table

On the way back we spent some time in Longreach to check out the Qantas museum and give ourselves some recovery time. Then Cunnamulla, Griffith followed by arriving at our home in Melbourne. This totalled 2,627km’s from Birdsville.

selfie of myself and geordie standing on the wing of an aircraft at the qantas muesum

The crossing

Much like the CSR, the real challenge in these journey comes to preparation. If you nail your planning, packing and other prep work, the journey itself is uneventful and breeze. Take your time on all the dunes and don’t try to push yourself or the equipment.

We had a good idea from other peoples crossings what to expect and a good idea from our own previous journeys what works for us. We didn’t change much of our load out from the CSR which made prep super simple for us. Not much in the way of new equipment or requirements. The middle of the desert isn’t the place where you want to experiment.

view down one of the dunes towards another. There is low level spinfix but not trees. the track is orange and straight, with very little deviation

The dunes are so large and long and it’s impossible to express their size purely in a photo

I was amazed about how different the dunes were between the lines and how they changed as we progressed east. For the most part we just crawled up each dune. There were maybe 4-5 dunes (including “big red”) where we needed to use a little bit of momentum to get over - the rest we could do at very low speed. This protects our car, makes for a comfy ride and prevent further damage to the track.

eyre creek crossing showing just a small puddle with me tracks running around in the dry

Surprisingly Eyre Creek had completely dried by the time we arrived so we didn’t require any water crossings this trip!

the top of big red covered in just sand. the top is flat and a 4x4 is sitting on top with people standing around talking

‘Big Red’ is the final dune of a typical Simpson Desert crossing. 30ish meters tall this dune is one of the hardest to cross, but technically not very challenging when approached carefully. It’s probably best to try crossing this dune like you would usually would the first time to get and understanding of the challenge before making adjustments.

You’ll soon realise that crawling over isn’t viable for ‘Big Red’. Air down. Right down. Most vehicles will be fine down to like 8 psi, so don’t be afraid to drop those tyre pressures down to 12 or even 10. The top is very soft, you won’t rock around in wombat holes at the top so once you get passed the corrugated sections just put your foot down. The top is flat so you’ll be fine at the top.

I tried a few different configurations for the car (have some fun and do some laps). I found that traction control has zapping a bit of energy at the top - turning this entirely off helped a lot with some of the harder approaches. Diff locked, 4low, gogogo.

Weather

It turns out that while we expected fairly cool evenings and mornings the desert had other ideas. Many towns around the Simpson desert broke their hottest August temperature records. Our extra blankets weren’t used and rarely did I need to wear my hoodie. The middle of the days were extremely hot, as such we used that time for mostly travel. Even lunch breaks were brief. Unlike the CSR with wells and trees - there was very little shade.

Fuel

We used the final CSR fuel consumption readings to calculate our expected fuel usage (22.8l/100km). We rounded up to the nearest jerry can - this meant full tank + 4 jerry cans. We had some extra carrying capacity so took an additional 2 jerry cans just in case. 6 in total. Four of these were stored in the vehicle and 2 on the roof.

Priority was to get the roof jerry cans off, so as soon as fuel tank had room these went inside. You want the roof rack as light as possible as the forces applied when hitting wombat holes at the top of dunes are significant. Many people crossing the Simpson have stories of roof racks breaking and to prove the point some travellers we came across had just that happen. Most of their fuel was stored on the roof rack.

Using the final CSR fuel consumption readings were pretty much spot on for the Simpson. 204 litres over roughly 950km = 21.4l/100km.

But our story about fuel doesn’t end here. It seems that the fuel we picked up back in Adelaide had some sort of contamination. The differential pressure sensor triggered on the fuel filter just past Dalhousie on the French line. We packed two fuel filters as we are well aware of the risks of filling up from jerry cans. The thing is though that the fuel filter warning had come on before we had emptied any jerry cans.

The light is only a warning, you’d typically just get it sorted as part of the next service. We tend to ignore this warning as working on the fuel system in a dusty environment in the middle of desert can be risky. We continued on without noticing much and in Birdsville replaced the filter - without clearing out the housing.

fuel filter housing showing dirt and goo inside where the filter is usually housed
The problem got a bit more serious between Birdsville and Longreach. Accelerating to highway speed resulting in the car lurching. After some thinking and troubleshooting we determined it was likely the fuel filter. Initially we just thought it might be dirt/dust build up in the filter and replaced it in the next town, along with cleaning out the housing. The housing was filled with a whitish goo. This solved the initial problem and we started monitoring the fuel rail pressure to provide an early warning system.

It wasn’t long however until the fuel filter warning came on again and eventually the same problem - fuel rail pressure preventing the car from accelerating quickly. We replaced the filter again. As we replaced bad fuel with good fuel the problem started occurring much further apart. At this stage we think we are mostly clear of the issue, however expect to go through a few more filters as the final crap works its way through the system.

Other faults

We had traction control fault occur several times. The timing of this aligned with when the HF APRS radio transmitted. Tightening up the sand flag antenna and adjusting the feed line seemed to resolve this mostly.

upper control arm ball joint boot covered in grease

Upper control arm ball joint boots seem to be oozing grease. These might be over packed or some other issue - still needs to be investigated.

grease covered area around the CV joint

Drivers side CV boot seems to be leaking grease. This also needs to be investigated - an obvious hole or puncture couldn’t be spotted this time.

Food

Something new for us this trip is my decision to eat entirely Vegan. Geordie being extremely supportive in this decision mostly ate the same meals as myself as we don’t like to cook two different things. An amazing side effect of this is that vegan produce often can be stored warm and has long shelf lives. This left our fridge/freeze much more empty than typical allowing us to freeze some bread and have plenty of room for cold drinks. Next time I might consider packing some ice-creams.

wraps loaded with filling with the desert sunset as a background

Some of the meals we had were:

  • Tacos
  • Nachos
  • Spaghetti
  • Rice
  • Wraps
  • Pancakes
  • Bagels
  • Burgers

None of these meals are vegan specific but since most of the core ingredients come in tins (veggies, beans, jackfruit, lentils) packing became a breeze.

Car to car wifi

Another interesting addition was Rusty’s Starlink. While I don’t see myself signing up to Musk’s service anytime soon I was very impressed with how well it worked. The dish was mounted flat on their roof rack and it was able to be used while moving.

Geordie came up with the idea of setting up a point to point wifi link between the two cars. This worked well when we had near line of sight (dropping past a dune or two meant that the signal would be lost but would quickly recover when the other vehicle reached the top of the dune) and provided internet throughout our journey.

An interesting side effect is that we could use ping -A to create an audible alert noise whenever we dropped out of range. A useful indicator to know if we were getting too far ahead and need to slow down.

A future experiment might be car to car messaging/warning service.

Tent rope lights

Finally one other little addition from an unlikely source. I bought some “Fishing rod lights” for another project that didn’t work out. These glow a nice green and red light and last an entire night. I placed these on the ropes on our swag which prevented tripping on them. Something I’m extremely good at doing.

swag with the ropes illuminated by the led lights

it’s not that bright in person!

Photos

That’s about it for my write up. I’m almost certain I’ve forgotten to mention a bunch of things - this adventure contained so much. If I remember anything I’ll be sure to make a follow up post here.

In the meantime if you wanted to check out some of the pictures I took of the journey I’ve uploaded them to flickr.