Swisscom 2025 security.txt challenge: full writeup


In Accidentally solving the Swisscom 2025 security.txt challenge I briefly hinted at the steps that I took to solve the challenge.

Seeing that Antoine published full writeup, here’s mine:

#!/usr/bin/env ruby

=begin
What's this?

https://github.com/swisscom/securitytxt/blob/master/challenges/2025.woz

How did I solve this? (how to replicate)

1. convert 2025.woz to a.dsk using https://github.com/leesaudan2/woz2dsk
1. patch the dsk with this file
1. load the a.dsk up in https://www.scullinsteel.com/apple2/
   (or https://github.com/AppleWin/AppleWin)
1. type in "brun flag" (you will get flag)

Useful stuff:

- dos fs description: http://fileformats.archiveteam.org/wiki/Apple_DOS_file_system
- 6502 disassembler: https://masswerk.at/6502/disassembler.html
- a2tools to dump stuff from .dsk: https://github.com/seanwallawalla/CatsEye_A2Tools
- debugging some of the hangs via `call -151` and then 80.99 (to dump memory)

=end

require 'pp'

def read_ts(file, track, sector)
  file.seek((track*16+sector)*256)
  file.read(256)
end

def write_ts(file, track, sector, offset, bytes)
  file.seek((track*16+sector)*256 + offset)
  file.write(bytes)
end

def human_name(name)
  name.map { |x| (x ^ 0x80).chr }.join.strip
end

File.open('a.dsk', 'r+b:ascii-8bit') do |f|
  # read catalog, figure out where it lives
  sec = read_ts(f, 17, 0).bytes
  # read first page of catalog (directory), find files
  cat = read_ts(f, sec[1], sec[2]).bytes
  for file in cat[0xb..-1].each_slice(35).to_a
    track, sector, type = *file[0,3]
    name = file[3,30]
    len = file[0x21,2]
    #p [track, sector, type, len, human_name(name)] unless [0, 0xff].include?(track)
    if ['FLAG', 'GLOBAL THERMONUCLEAR WAR'].include?(human_name(name))
      puts human_name(name)
      pp({ts: [track, sector], type: type, len: len})
      # get tracklist
      tl = read_ts(f, track, sector).bytes # tracklist
      #p tl
      next_tl = tl[1,2]
      sec_off = tl[5,2] # sector offset
      pp({next_tl: next_tl, sec_off: sec_off})
      file_sectors = tl[0xc..-1].each_slice(2).to_a.reject { |x| x == [0,0] }
      p :data
      for sec in file_sectors
	pp({sector: sec})
	sec_data = read_ts(f, *sec).bytes
	pp({sector_data: sec_data[0,15]})
	break
      end

      # patch it?
      if human_name(name) == 'FLAG'
	write_ts(f, *file_sectors.first, 0, [
	  3, 8, # load addr
	  0xff-4, 80, # size
	  0x20, 0x58, 0xfc, # clr scrn
	  # this took a while to debug... otherwise it was hanging
	  # but strangely typing `X` before the `brun flag` made it
	  # work. so then just grep for `LDA \$..$`, dump, and minify
	  0xa9, 0x58, # lda ...
	  0x85, 0x81, # sta 81
	  0x20, 0, 0x40, # JSR $4000
	  0x60, # rts
	  # FIXME it hangs after displaying flag and <Esc>, but meh
	].pack('C*'))
	pp({patched: read_ts(f, *file_sectors.first).bytes[0,15]})
	puts %[Patching done, now load it up... then: "brun flag"]
      end

      puts
    end
  end
end

Closing words

Antoine’s writeup is much more thorough than this… and actually patches this whole thing at WOZ level. While mine just accidentally recovers just enough to get the whole thing running.

Well, oopsie. :-D

Btw, the 2026 edition is out. And it was significantly easier (for me). Give it a try?